Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Anmerkung
Dieser Artikel ist eine Featurespezifikation. 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 Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.
Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.
Champion Issue: https://github.com/dotnet/csharplang/issues/7608
Zusammenfassung
Dieser Vorschlag erweitert die Funktionen von ref struct so, dass sie Schnittstellen implementieren und als generische Typargumente teilnehmen können.
Motivation
Die Unfähigkeit von ref struct, Schnittstellen zu implementieren, bedeutet, dass sie nicht an relativ grundlegenden Abstraktionstechniken von .NET teilnehmen können. Ein Span<T> kann, obwohl es alle Attribute einer sequentiellen Liste hat, nicht an Methoden teilnehmen, die IReadOnlyList<T>, IEnumerable<T>, etc. nehmen ... Stattdessen müssen spezifische Methoden für Span<T> gecodet werden, die praktisch die gleiche Implementierung haben. Indem ref struct Schnittstellen implementiert, können Vorgänge über sie abstrahiert werden, wie es für andere Typen der Fall ist.
Detailentwurf
ref struct Schnittstellen
Die Sprache ermöglicht es ref struct-Typen, Schnittstellen zu implementieren. Die Syntax und die Regeln sind identisch mit den normalen struct, mit einigen Ausnahmen, um die Einschränkungen von Typen von ref struct zu berücksichtigen.
Die Möglichkeit, Schnittstellen zu implementieren, hat keine Auswirkungen auf die bestehenden Beschränkungen für das Boxen von ref struct-Instanzen. Das heißt, selbst wenn eine ref struct eine bestimmte Schnittstelle implementiert, kann sie nicht direkt auf diese Schnittstelle gecastet werden, da dies eine Boxing-Aktion darstellt.
ref struct File : IDisposable
{
private SafeHandle _handle;
public void Dispose()
{
_handle.Dispose();
}
}
File f = ...;
// Error: cannot box `ref struct` type `File`
IDisposable d = f;
Die Möglichkeit, Schnittstellen zu implementieren, ist nur sinnvoll, wenn sie mit der Möglichkeit kombiniert wird, dass ref struct an generischen Argumenten teilnehmen kann (wie später erläutert).
Um Schnittstellen die volle Ausdruckskraft eines ref struct und die Probleme mit der Lebensdauer zu bieten, die sie mit sich bringen können, lässt die Sprache die Möglichkeit zu, dass [UnscopedRef] auf Schnittstellenmethoden und -eigenschaften erscheint. Dies ist notwendig, da es Schnittstellen ermöglicht, die über struct abstrahieren, dieselbe Flexibilität wie die direkte Verwendung eines struct zu haben. Betrachten Sie das folgende Beispiel:
interface I1
{
[UnscopedRef]
ref int P1 { get; }
ref int P2 { get; }
}
struct S1
{
[UnscopedRef]
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
ref int M<T>(T t, S1 s)
where T : I1, allows ref struct
{
// Error: may return ref to t
return ref t.P1;
// Error: may return ref to t
return ref s.P1;
// Okay
return ref t.P2;
// Okay
return ref s.P2;
}
Wenn ein struct / ref struct Mitglied ein Schnittstellenmitglied mit einem [UnscopedRef]-Attribut implementiert, kann das implementierende Mitglied auch mit [UnscopedRef] versehen werden, ist aber nicht erforderlich. Ein Mitglied mit [UnscopedRef] darf jedoch nicht verwendet werden, um ein Mitglied zu implementieren, dem das Attribut fehlt (Details).
interface I1
{
[UnscopedRef]
ref int P1 { get; }
ref int P2 { get; }
}
struct S1
{
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
struct S2
{
[UnscopedRef]
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
struct S3 : I1
{
internal ref int P1 { get {...} }
// Error: P2 is marked with [UnscopedRef] and cannot implement I1.P2 as is not marked
// with [UnscopedRef]
[UnscopedRef]
internal ref int P2 { get {...} }
}
class C1 : I1
{
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
Standardschnittstellenmethoden stellen ein Problem für ref struct dar, da es keinen Schutz gegen die Standardimplementierung gibt, die das this Mitglied verpackt.
interface I1
{
void M()
{
// Danger: both of these box if I1 is implemented by a ref struct
I1 local1 = this;
object local2 = this;
}
}
// Error: I1.M cannot implement interface member I1.M() for ref struct S
ref struct S : I1 { }
Um dies zu handhaben, wird ein ref struct gezwungen, alle Mitglieder einer Schnittstelle zu implementieren, auch wenn sie Standardimplementierungen haben.
Die Laufzeit wird auch aktualisiert, um eine Ausnahme auszuwerfen, wenn ein Standardschnittstellenelement vom Typ ref struct aufgerufen wird.
Um eine Ausnahme zur Laufzeit zu vermeiden, meldet der Compiler einen Fehler für einen Aufruf einer nicht virtuellen Instanzmethode (oder -eigenschaft) bei einem Typparameter, der Ref Strukt zulässt. Hier ist ein Beispiel:
public interface I1
{
sealed void M3() {}
}
class C
{
static void Test2<T>(T x) where T : I1, allows ref struct
{
#line 100
x.M3(); // (100,9): error: A non-virtual instance interface member cannot be accessed on a type parameter that allows ref struct.
}
}
Es gibt auch eine offene Entwurfsfrage bezüglich des Meldens einer -Warnung beim Aufruf einer virtuellen (nicht abstrakten) Instanzmethode (oder -eigenschaft) auf einem Typparameter, der Ref-Struct zulässt.
Detaillierte Hinweise:
- Ein
ref structkann eine Schnittstelle implementieren - Ein
ref structkann nicht an den Mitgliedern einer Standardschnittstelle teilnehmen - Ein
ref structkann nicht auf Schnittstellen gecastet werden, die es implementiert, da dies eine Boxing-Operation ist.
ref struct Generische Parameter
type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;
type_parameter_constraints
: restrictive_type_parameter_constraints
| allows_type_parameter_constraints_clause
| restrictive_type_parameter_constraints ',' allows_type_parameter_constraints_clause
restrictive_type_parameter_constraints
: primary_constraint
| secondary_constraints
| constructor_constraint
| primary_constraint ',' secondary_constraints
| primary_constraint ',' constructor_constraint
| secondary_constraints ',' constructor_constraint
| primary_constraint ',' secondary_constraints ',' constructor_constraint
;
primary_constraint
: class_type
| 'class'
| 'struct'
| 'unmanaged'
;
secondary_constraints
: interface_type
| type_parameter
| secondary_constraints ',' interface_type
| secondary_constraints ',' type_parameter
;
constructor_constraint
: 'new' '(' ')'
;
allows_type_parameter_constraints_clause
: 'allows' allows_type_parameter_constraints
allows_type_parameter_constraints
: allows_type_parameter_constraint
| allows_type_parameter_constraints ',' allows_type_parameter_constraint
allows_type_parameter_constraint
: ref_struct_clause
ref_struct_clause
: 'ref' 'struct'
Die Sprache ermöglicht es generischen Parametern, ref struct als Argumente zu unterstützen, indem die allows ref struct Syntax in einer where Klausel verwendet wird:
T Identity<T>(T p)
where T : allows ref struct
=> p;
// Okay
Span<int> local = Identity(new Span<int>(new int[10]));
Dies ähnelt anderen Elementen in einer where-Klausel, in der sie die Funktionen des generischen Parameters angibt. Der Unterschied besteht darin, dass andere Syntaxelemente den Satz von Typen einschränken, die einen generischen Parameter erfüllen können, während allows ref struct die Gruppe von Typen erweitert. Dies ist effektiv eine Anti-Einschränkung, da sie die implizite Einschränkung entfernt, dass ref struct keinen generischen Parameter erfüllen kann. Daher erhält dies ein neues Syntaxpräfix, allows, um das deutlicher zu machen.
Ein von allows ref struct gebundener Typparameter weist alle Verhaltensweisen eines ref struct Typs auf:
- Instanzen von A können nicht gecastet werden
- Instanzen nehmen an Lifetime-Rules teil wie eine normale
ref struct-Instanz - Der Typparameter kann nicht in
staticFeldern, Elementen eines Arrays usw. verwendet werden... - Instanzen können mit
scopedgekennzeichnet werden.
Beispiele für diese Regeln in Aktion:
interface I1 { }
I1 M1<T>(T p)
where T : I1, allows ref struct
{
// Error: cannot box potential ref struct
return p;
}
T M2<T>(T p)
where T : allows ref struct
{
Span<int> span = stackalloc int[42];
// The safe-to-escape of the return is current method because one of the inputs is
// current method
T t = M3<int, T>(span);
// Error: the safe-to-escape is current method.
return t;
// Okay
return default;
return p;
}
R M3<T, R>(Span<T> span)
where R : allows ref struct
{
return default;
}
Die Anti-Beschränkung wird nicht von einer Typ-Parameter-Typ-Beschränkung "geerbt".
Beispielsweise kann S im folgenden Code nicht durch eine ref struct ersetzt werden.
class C<T, S>
where T : allows ref struct
where S : T
{}
Detaillierte Hinweise:
- Ein generischer
where T : allows ref structParameter kann nicht-
where T : Uhaben, wobeiUein bekannter Referenztyp ist -
where T : classEinschränkung haben - Kann nicht als generisches Argument verwendet werden, es sei denn, der entsprechende Parameter ist auch
where T: allows ref struct
-
- Die
allows ref structmuss die letzte Einschränkung in derwhere-Klausel sein. - Ein Typparameter
T, derallows ref structhat, hat alle gleichen Einschränkungen wie einref structTyp.
Darstellung in Metadaten
Typ-Parameter, die ref structs zulassen, werden in den Metadaten kodiert, wie im byref-like generics doc beschrieben. Insbesondere durch Verwendung des CorGenericParamAttr.gpAllowByRefLike(0x0020) oder System.Reflection.GenericParameterAttributes.AllowByRefLike(0x0020) Flag-Wertes.
Es kann festgestellt werden, ob die Laufzeit das Feature unterstützt, indem das Vorhandensein des System.Runtime.CompilerServices.RuntimeFeature.ByRefLikeGenerics-Feldes überprüft wird.
Die APIs wurden in https://github.com/dotnet/runtime/pull/98070hinzugefügt.
using Anweisung
Eine using-Anweisung erkennt und verwendet die Implementierung der IDisposable-Schnittstelle, wenn die Ressource ein ref struct ist.
ref struct S2 : System.IDisposable
{
void System.IDisposable.Dispose()
{
}
}
class C
{
static void Main()
{
using (new S2())
{
} // S2.System.IDisposable.Dispose is called
}
}
Beachten Sie, dass eine Dispose Methode bevorzugt wird, die das Muster implementiert, und nur, wenn eins nicht gefunden wird, IDisposable Implementierung verwendet wird.
Eine using-Anweisung erkennt und verwendet die Implementierung der IDisposable-Schnittstelle, wenn die Ressource ein Typparameter ist, der allows ref struct und IDisposable in seinem Set der effektiven Schnittstellen festgelegt ist.
class C
{
static void Test<T>(T t) where T : System.IDisposable, allows ref struct
{
using (t)
{
}
}
}
Beachten Sie, dass eine Methode mit Muster Dispose bei einem Typparameter nicht erkannt wird, der allows ref struct ist, da eine Schnittstelle (und dies ist der einzige Ort, an dem wir möglicherweise nach einem Muster suchen könnten) keine Referenzstruktur ist.
interface IMyDisposable
{
void Dispose();
}
class C
{
static void Test<T>(T t, IMyDisposable s) where T : IMyDisposable, allows ref struct
{
using (t) // Error, the pattern is not recognized
{
}
using (s) // Error, the pattern is not recognized
{
}
}
}
await using Anweisung
Aktuell verbietet die Sprache die Verwendung von ref Structs als Ressourcen in await using-Anweisungen. Die gleiche Einschränkung gilt für einen Typ-Parameter, der allows ref struct.
Es gibt einen Vorschlag zur Aufhebung der allgemeinen Beschränkungen für die Verwendung von ref structs in asynchronen Methoden – https://github.com/dotnet/csharplang/pull/7994.
Der restliche Abschnitt beschreibt das Verhalten, nachdem die allgemeine Einschränkung für die await using-Anweisung aufgehoben wird, wenn bzw. wann dies geschieht.
Eine await using Anweisung wird die Implementierung der IAsyncDisposable Schnittstelle erkennen und verwenden, wenn die Ressource ein ref struct ist.
ref struct S2 : IAsyncDisposable
{
ValueTask IAsyncDisposable.DisposeAsync()
{
}
}
class C
{
static async Task Main()
{
await using (new S2())
{
} // S2.IAsyncDisposable.DisposeAsync
}
}
Beachten Sie, dass eine DisposeAsync Methode bevorzugt wird, die das Muster implementiert, und nur, wenn eins nicht gefunden wird, IAsyncDisposable Implementierung verwendet wird.
Eine strukturierte Methode DisposeAsync wird für einen Typ-Parameter allows ref struct erkannt, so wie sie heute für Typ-Parameter ohne diese Einschränkung erkannt wird.
interface IMyAsyncDisposable
{
ValueTask DisposeAsync();
}
class C
{
static async Task Test<T>() where T : IMyAsyncDisposable, new(), allows ref struct
{
await using (new T())
{
} // IMyAsyncDisposable.DisposeAsync
}
}
Eine using-Anweisung erkennt und verwendet die Implementierung der IAsyncDisposable-Schnittstelle, wenn die Ressource ein Typparameter ist, der allows ref struct, die Suche nach der DisposeAsync-Mustermethode fehlgeschlagen ist und IAsyncDisposable im Set der effektiven Schnittstellen des Typparameters liegt.
interface IMyAsyncDisposable1
{
ValueTask DisposeAsync();
}
interface IMyAsyncDisposable2
{
ValueTask DisposeAsync();
}
class C
{
static async Task Test<T>() where T : IMyAsyncDisposable1, IMyAsyncDisposable2, IAsyncDisposable, new(), allows ref struct
{
await using (new T())
{
System.Console.Write(123);
} // IAsyncDisposable.DisposeAsync
}
}
foreach Anweisung
Der abschnitt https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement sollte entsprechend aktualisiert werden, um Folgendes zu integrieren.
Eine foreach-Anweisung erkennt und verwendet die Implementierung der IEnumerable<T>/IEnumerable-Schnittstelle, wenn Collection ein ref Struct ist.
ref struct S : IEnumerable<int>
{
IEnumerator<int> IEnumerable<int>.GetEnumerator() {...}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {...}
}
class C
{
static void Main()
{
foreach (var i in new S()) // IEnumerable<int>.GetEnumerator
{
}
}
}
Eine strukturierte Methode GetEnumerator wird für einen Typ-Parameter allows ref struct erkannt, so wie sie heute für Typ-Parameter ohne diese Einschränkung erkannt wird.
interface IMyEnumerable<T>
{
IEnumerator<T> GetEnumerator();
}
class C
{
static void Test<T>(T t) where T : IMyEnumerable<int>, allows ref struct
{
foreach (var i in t) // IMyEnumerable<int>.GetEnumerator
{
}
}
}
Eine foreach-Anweisung erkennt und verwendet die Implementierung der IEnumerable<T>/IEnumerableSchnittstelle, wenn Collection ein Typ-Parameter ist, bei dem allows ref struct die Suche nach der GetEnumeratorMustermethode fehlgeschlagen ist und IEnumerable<T>/IEnumerable sich im Set der effektiven Schnittstellen des Typ-Parameters befindet.
interface IMyEnumerable1<T>
{
IEnumerator<int> GetEnumerator();
}
interface IMyEnumerable2<T>
{
IEnumerator<int> GetEnumerator();
}
class C
{
static void Test<T>(T t) where T : IMyEnumerable1<int>, IMyEnumerable2<int>, IEnumerable<int>, allows ref struct
{
foreach (var i in t) // IEnumerable<int>.GetEnumerator
{
}
}
}
Ein enumerator-Muster wird für einen Typ-Parameter erkannt, der allows ref struct ist, wie es heute für Typ-Parameter ohne diese Einschränkung erkannt wird.
interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct
{
TEnumerator GetEnumerator();
}
class C
{
static void Test1<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
where TEnumerator : IEnumerator, IDisposable, allows ref struct
{
foreach (var i in t) // IEnumerator.MoveNext/Current
{
}
}
static void Test2<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
where TEnumerator : IEnumerator<int>, allows ref struct
{
foreach (var i in t) // IEnumerator<int>.MoveNext/Current
{
}
}
static void Test3<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
where TEnumerator : IMyEnumerator<int>, allows ref struct
{
foreach (var i in t) // IMyEnumerator<int>.MoveNext/Current
{
}
}
}
interface IMyEnumerator<T> : System.IDisposable
{
T Current {get;}
bool MoveNext();
}
Eine foreach-Anweisung erkennt und verwendet die Implementierung der IDisposable-Schnittstelle, wenn der Enumerator ein ref struct ist.
struct S1
{
public S2 GetEnumerator()
{
return new S2();
}
}
ref struct S2 : System.IDisposable
{
public int Current {...}
public bool MoveNext() {...}
void System.IDisposable.Dispose() {...}
}
class C
{
static void Main()
{
foreach (var i in new S1())
{
} // S2.System.IDisposable.Dispose()
}
}
Beachten Sie, dass eine Dispose Methode bevorzugt wird, die das Muster implementiert, und nur, wenn eins nicht gefunden wird, IDisposable Implementierung verwendet wird.
Eine foreach-Anweisung erkennt und verwendet die Implementierung der IDisposable-Schnittstelle, wenn Enumerator ein Typparameter ist, der allows ref struct und IDisposable in seinem Set der effektiven Schnittstellen festgelegt ist.
interface ICustomEnumerator
{
int Current {get;}
bool MoveNext();
}
interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct
{
TEnumerator GetEnumerator();
}
class C
{
static void Test<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>
where TEnumerator : ICustomEnumerator, System.IDisposable, allows ref struct
{
foreach (var i in t)
{
} // System.IDisposable.Dispose()
}
}
Beachten Sie, dass eine Methode mit Muster Dispose bei einem Typparameter nicht erkannt wird, der allows ref struct ist, da eine Schnittstelle (und dies ist der einzige Ort, an dem wir möglicherweise nach einem Muster suchen könnten) keine Referenzstruktur ist.
Da die Runtime keine Möglichkeit bietet, zu überprüfen, ob zur Laufzeit ein Typparameter, der allows ref struct die Schnittstelle IDisposable implementiert, einen Typparameter-Enumerator, der allows ref struct ist, nicht zulässt, es sei denn, IDisposable befindet sich in seinem effektiven Set von Schnittstellen.
interface ICustomEnumerator
{
int Current {get;}
bool MoveNext();
}
interface IMyDisposable
{
void Dispose();
}
interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct
{
TEnumerator GetEnumerator();
}
class C
{
static void Test<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>
where TEnumerator : ICustomEnumerator, IMyDisposable, allows ref struct
{
// error CS9507: foreach statement cannot operate on enumerators of type 'TEnumerator'
// because it is a type parameter that allows ref struct and
// it is not known at compile time to implement IDisposable.
foreach (var i in t)
{
}
}
}
await foreach Anweisung
Der abschnitt https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement sollte entsprechend aktualisiert werden, um Folgendes zu integrieren.
Eine await foreach-Anweisung erkennt und verwendet die Implementierung der IAsyncEnumerable<T>-Schnittstelle, wenn Collection ein ref Struct ist.
ref struct S : IAsyncEnumerable<int>
{
IAsyncEnumerator<int> IAsyncEnumerable<int>.GetAsyncEnumerator(CancellationToken token) {...}
}
class C
{
static async Task Main()
{
await foreach (var i in new S()) // S.IAsyncEnumerable<int>.GetAsyncEnumerator
{
}
}
}
Eine strukturierte Methode GetAsyncEnumerator wird für einen Typ-Parameter allows ref struct erkannt, so wie sie heute für Typ-Parameter ohne diese Einschränkung erkannt wird.
interface IMyAsyncEnumerable<T>
{
IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
class C
{
static async Task Test<T>() where T : IMyAsyncEnumerable<int>, allows ref struct
{
await foreach (var i in default(T)) // IMyAsyncEnumerable<int>.GetAsyncEnumerator
{
}
}
}
Eine await foreach-Anweisung erkennt und verwendet die Implementierung der IAsyncEnumerable<T>-Schnittstelle, wenn Collection ein Typparameter ist, der allows ref struct, die Suche nach der GetAsyncEnumerator-Mustermethode fehlgeschlagen ist und IAsyncEnumerable<T> sich im Set der effektiven Schnittstellen des Typparameters befindet.
interface IMyAsyncEnumerable1<T>
{
IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
interface IMyAsyncEnumerable2<T>
{
IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
class C
{
static async Task Test<T>() where T : IMyAsyncEnumerable1<int>, IMyAsyncEnumerable2<int>, IAsyncEnumerable<int>, allows ref struct
{
await foreach (var i in default(T)) // IAsyncEnumerable<int>.GetAsyncEnumerator
{
System.Console.Write(i);
}
}
}
Eine await foreach Anweisung wird weiterhin einen ref struct enumerator und einen type parameter enumerator verbieten, der allows ref struct. Der Grund dafür ist die Tatsache, dass der Enumerator über await MoveNextAsync() Aufrufe hinweg erhalten bleiben muss.
Delegattyp für die anonyme Funktion oder Methodengruppe
Der Abschnitt https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/lambda-improvements.md#delegate-types besagt:
Der Compiler kann in Zukunft mehr Signaturen unterstützen, die an
System.Action<>- undSystem.Func<>-Typen gebunden sind, wenn z.B.ref struct-Typen als Typargumente zulässig sind.
Action<> und Func<> Typen mit allows ref struct Beschränkungen für ihre Typparameter werden in mehr Szenarien mit ref struct Typen in der Signatur des Delegaten verwendet.
Wenn die Ziellaufzeit allows ref struct-Bedingungen unterstützt, enthalten die generischen anonymen Delegattypen die allows ref struct-Bedingung für ihre Typparameter. Dies ermöglicht die Ersetzung dieser Typparameter durch ref struct Typen und andere Typparameter mit allows ref struct Einschränkung.
Inlinearrays
Der Abschnitt https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/inline-arrays.md#detailed-design besagt:
Die Sprache wird einen type-safe/ref-safe Weg für den Zugriff auf die Elemente von Inline-Array-Typen bieten. Der Zugriff wird auf Basis der Spannweite erfolgen. Dies beschränkt die Unterstützung auf Inline-Array-Typen mit Elementtypen, die als Typargument verwendet werden können.
Wenn Span-Typen geändert werden, um Spans von ref structs zu unterstützen, sollte die Einschränkung für Inline-Arrays von ref structs aufgehoben werden.
Schlüssigkeit
Wir möchten die Solidität sowohl der ref struct-Antieinschränkung im Besonderen als auch des Antieinschränkungskonzepts im Allgemeinen überprüfen. Zu diesem Zweck möchten wir die bestehenden Beweise für die Schlüssigkeit des C#-Typsystems nutzen. Diese Aufgabe wird vereinfacht, indem eine neue Sprache definiert wird, die C# ähnelt, aber in ihrer Konstruktion regelmäßiger ist. Wir werden die Sicherheit dieses Modells überprüfen und dann eine Soundübersetzung in diese Sprache angeben. Da sich diese neue Sprache auf Einschränkungen konzentriert, nennen wir diese Sprache "constraint-C#".
Die primäre Sicherheitsinvariante für Referenzstrukturen, die erhalten werden muss, besteht darin, dass Variablen des Referenzstrukturtyps nicht auf dem Heap erscheinen dürfen. Wir können diese Einschränkung über eine Einschränkung codieren. Da Einschränkungen die Ersetzung ermöglichen, sie nicht zu verbieten, werden wir technisch die umgekehrte Einschränkung definieren: heap. Die heap Einschränkung gibt an, dass ein Typ auf dem Heap angezeigt werden kann. In "constraint-C#" erfüllen alle Typen die heap-Einschränkung außer für ref-structs. Darüber hinaus werden alle vorhandenen Typparameter in C# auf Typparameter mit der heap Einschränkung in "constraint-C#" reduziert.
Vorausgesetzt, dass das bestehende C# sicher ist, können wir die C#-ref-struct-Regeln auf "constraint-C#" übertragen.
- Felder von Klassen können nicht den Typ ref-struct haben.
- Statische Felder können keinen ref-struct-Typ haben.
- Variablen des Referenzstrukturtyps können nicht in Nicht-Ref-Strukturen konvertiert werden.
- Variablen des Referenzstrukturtyps können nicht als Typargumente ersetzt werden.
- Variablen des Referenzstrukturtyps können keine Schnittstellen implementieren.
Die neuen Regeln gelten für die heap Einschränkung:
- Felder von Klassen müssen Typen aufweisen, die die
heapEinschränkung erfüllen. - Statische Felder müssen Typen aufweisen, die die
heapEinschränkung erfüllen. - Typen mit der
heap-Beschränkung haben nur die Identitätskonvertierung. - Variablen des Referenzstrukturtyps können nur ohne die
heapEinschränkung durch Typparameter ersetzt werden. - Referenzstrukturtypen können nur Schnittstellen ohne Standardschnittstellenmitglieder implementieren.
Regeln (4) und (5) werden leicht geändert. Beachten Sie, dass regel (4) nicht genau übertragen werden muss, da wir einen Begriff von Typparametern ohne den heap Contraint haben. Regel (5) ist kompliziert. Die Implementierung von Schnittstellen ist nicht generell unsinnig, aber Standardschnittstellenmethoden implizieren einen Empfänger vom Typ Schnittstelle, der ein Nicht-Wert-Typ ist und gegen Regel (3) verstößt. Daher sind Standard-Interface-Mitglieder unzulässig.
Mit diesen Regeln ist "constraint-C#" ref-struct sicher, unterstützt die Typsubstitution und unterstützt die Implementierung von Schnittstellen. Der nächste Schritt besteht darin, die in diesem Vorschlag definierte Sprache zu übersetzen, die wir möglicherweise "allow-C#" in "constraint-C#" nennen. Glücklicherweise ist dies trivial. Die Senkung ist eine einfache syntaktische Transformation. Die Syntax where T : allows ref struct in "allow-C#" entspricht in "constraint-C#" keiner Einschränkung, und das Fehlen von "allow clauses" entspricht der heap Einschränkung. Da die abstrakte Semantik und die Typisierung äquivalent sind, ist "allow-C#" ebenfalls eine solide Lösung.
Es gibt eine letzte Eigenschaft, die wir möglicherweise berücksichtigen können: Ob alle eingegebenen Begriffe in C# auch in "constraint-C#" eingegeben werden. Mit anderen Worten, wir wollen wissen, ob für alle Terme t in C# der entsprechende Term t' nach der Reduktion auf "constraint-C#" gut typisiert ist. Es handelt sich dabei nicht um eine Einschränkung der Schlüssigkeit – Begriffe, die in unserer Zielsprache schlecht getippt sind, würden niemals eine Unsicherheit zulassen -, sondern es geht um die Rückwärtskompatibilität. Wenn wir die Typisierung von "constraint-C#" verwenden, um "allow-C#" zu validieren, möchten wir bestätigen, dass dadurch kein bestehender C#-Code unzulässig wird.
Da alle C#-Terme als gültige "constraint-C#"-Terme beginnen, können wir die Bewahrung überprüfen, indem wir jede unserer neuen "constraint-C#"-Beschränkungen untersuchen. Erstens: Das Hinzufügen der heap-Bedingung. Da alle Typparameter in C# die heap Einschränkung abrufen würden, müssen alle vorhandenen Ausdrücke diese Einschränkung erfüllen. Dies gilt für alle konkreten Typen mit Ausnahme von Refstrukturen, was angemessen ist, da Refstrukturen heute nicht als Typargumente erscheinen dürfen. Es gilt auch für alle Typparameter, da sie selbst alle die heap-Beschränkung erhalten würden. Da die heap Einschränkung eine gültige Kombination mit allen anderen Einschränkungen ist, würde dies keine Probleme darstellen. Regeln (1-5) würden keine Probleme darstellen, da sie direkt den bestehenden C#-Regeln entsprechen oder eine Entspannung davon sind. Daher sollten alle typierbaren Begriffe in C# auch in "constraint-C#" typierbar sein, und wir sollten keine Änderungen einführen, die die Typisierung unterbrechen.
Offene Probleme
Anti-Constraint-Syntax
Entscheidung: where T: allows ref struct verwenden
In diesem Vorschlag wird die ref struct Anti-Constraint-Syntax durch die Erweiterung der bestehenden where-Syntax um allows ref struct offengelegt. Dies beschreibt das Feature prägnant und ist auch erweiterbar, um in Zukunft andere Antieinschränkungen wie Zeiger einzuschließen. Es gibt andere Lösungen, die es wert sind, zu diskutieren.
Die erste Möglichkeit besteht darin, eine andere Syntax in der where-Klausel zu wählen. Weitere vorgeschlagene Optionen sind enthalten:
-
~ref struct: Das~dient als Markierung dafür, dass die folgende Syntax eine Antieinschränkung darstellt. -
include ref struct: Verwenden vonincludesanstelle vonallows
void M<T>(T p)
where T : IDisposable, ~ref struct
{
p.Dispose();
}
Die zweite besteht darin, eine neue Klausel vollständig zu verwenden, um deutlich zu machen, dass im Folgenden die Gruppe zulässiger Typen erweitert wird. Die Befürworter dieses Vorschlags sind der Meinung, dass die Verwendung der Syntax innerhalb von where zu Verwirrung beim Lesen führen könnte. Der ursprüngliche Vorschlag hat die folgende Syntax verwendet: allow T: ref struct:
void M<T>(T p)
where T : IDisposable
allow T : ref struct
{
p.Dispose();
}
Die Syntax where T: allows ref struct hatte in LDM-Diskussionen eine etwas stärkere Vorliebe.
Co- und Contra-Varianz
Entscheidung: Keine neuen Probleme
Um maximal nützlich zu sein, müssen Typparameter, die allows ref struct sind, mit der generischen Varianz kompatibel sein. Insbesondere muss es für einen Parameter zulässig sein, sowohl co/contravariant als auch allows ref structzu sein. Andernfalls wären sie in vielen der beliebtesten delegate- und interface-Typen in .NET wie Func<T>, Action<T>, IEnumerable<T>, etc. nicht verwendbar.
Nach der Diskussion ist dies ein Nicht-Problem. Die allows ref struct Einschränkung ist nur eine andere Möglichkeit, wie struct als generische Argumente verwendet werden kann. Genau wie ein normales struct-Argument die Varianz einer API entfernt, wird es auch ein ref struct-Argument tun.
Automatische Anwendung auf delegierte Mitglieder
Entscheidung: Nicht automatisch anwenden
Für viele generische delegate Mitglieder könnte die Sprache automatisch allows ref struct anwenden, da es sich um eine reine Umkehrung handelt. Bedenken Sie, dass es für Delegates im Stil von Func<> / Action<> und die meisten Schnittstellendefinitionen keinen Nachteil hat, die Möglichkeit zu erweitern, ref struct zuzulassen. Die Sprache kann Regeln definieren, bei denen diese Gegeneinschränkung automatisch angewendet werden kann. Dadurch wird der manuelle Prozess entfernt und die Einführung dieses Features beschleunigt.
Diese automatische Anwendung von allows ref struct stellt jedoch einige Probleme dar. Das erste betrifft Szenarien mit mehreren Zielen. Code würde in einem Zielframework kompiliert, schlägt aber in einem anderen fehl, und es gibt keinen syntaktischen Indikator, warum sich die APIs anders verhalten sollten.
// Works in net9.0 but fails in all other TF
Func<Span<char>> func;
Dies wird wahrscheinlich zu Kundenverwirrung führen, und wenn sich die Kunden die Änderungen in Func<T> in der net9.0-Quelle anschauen, würde ihnen das keinen Hinweis darauf geben, welche Änderungen vorgenommen wurden.
Das andere Problem ist, dass sehr subtile Änderungen im Code gespenstische Wirkung auf Distanz Probleme verursachen können. Beachten Sie den folgenden Code:
interface I1<T>
{
}
Diese Schnittstelle wäre für die automatische Anwendung von allows ref structgeeignet. Wenn ein Entwickler zu einem späteren Zeitpunkt eine Standardschnittstellenmethode hinzufügt, dann wäre diese plötzlich nicht mehr vorhanden und würde alle Consumer, die bereits Aufrufe wie I1<Span<char>> erstellt haben, fehlerhaft machen. Dies ist eine sehr subtile Änderung, die schwer auffindbar wäre.
Binäre Breaking Changes
Das Hinzufügen von allows ref struct zu einer bestehenden API ist keine fehlerhafte Änderung. Es wird lediglich der Satz zulässiger Typen für eine API erweitert. Sie müssen herausfinden, ob dies eine fehlerhafte Änderung der Binärdateien ist oder nicht. Unklar, ob das Aktualisieren der Attribute eines generischen Parameters eine binäre Unterbrechungsänderung darstellt.
Warnung bei DIM-Aufruf
Sollte der Compiler bei folgendem Aufruf von M warnen, da er die Möglichkeit für eine Laufzeit-Ausnahme schafft?
interface I1
{
// Virtual method with default implementation
void M() { }
}
// Invocation of a virtual instance method with default implementation in a generic method that has the `allows ref struct`
// anti-constraint
void M<T>(T p)
where T : allows ref struct, I1
{
p.M(); // Warn?
}
Dies könnte jedoch in den meisten Szenarien laut und nicht sehr hilfreich sein. C# erfordert Referenzstruktur zum Implementieren aller virtuellen APIs. Vorausgesetzt, dass andere Spieler die gleiche Regel befolgen, ist die einzige Situation, in der dies zu einer Ausnahme führen kann, wenn die Methode nachträglich hinzugefügt wird. Der Autor des verbrauchenden Codes hat häufig keine Kenntnis von all diesen Details und hat oft keine Kontrolle über Ref-Strukturen, die vom Code verwendet werden. Daher ist die einzige Aktion, die der Autor wirklich ausführen kann, die Warnung zu unterdrücken.
Betrachtungen
Laufzeitunterstützung
Dieses Feature erfordert mehrere Unterstützungselemente aus dem Laufzeit-/Bibliothekenteam:
- Verhindern, dass Standardschnittstellenmethoden auf
ref structangewendet werden - API in
System.Reflection.Metadatazum Codieren desgpAcceptByRefLike-Werts - Unterstützung für generische Parameter, die ein
ref structsind
Die meisten dieser Unterstützung sind wahrscheinlich bereits vorhanden. Die allgemeine ref struct Unterstützung für generische Parameter ist bereits implementiert, wie hier beschrieben. Es ist möglich, dass die DIM-Implementierung bereits ref structberücksichtigt. Aber jedes dieser Elemente muss nachverfolgt werden.
API-Versionsverwaltung
lässt ref struct Anti-Constraint zu
Die allows ref struct Antieinschränkung kann sicher auf eine große Anzahl generischer Definitionen angewendet werden, die nicht über Implementierungen verfügen. Dies bedeutet, dass die meisten Delegaten, Schnittstellen und abstract-Methoden allows ref struct sicher auf ihre Parameter anwenden können. Dies sind nur API-Definitionen ohne Implementierungen und damit das Erweitern des Satzes zulässiger Typen führt nur zu Fehlern, wenn sie als Typargumente verwendet werden, bei denen ref struct nicht zulässig sind.
API-Besitzer können sich auf eine einfache Regel von "wenn sie kompiliert, es ist sicher" verlassen. Der Compiler gibt bei allen unsicheren Verwendungen von allows ref structeinen Fehler aus, genau wie bei anderen Verwendungen von ref struct.
Gleichzeitig sollten API-Autoren jedoch Überlegungen zur Versionsverwaltung berücksichtigen. Im Wesentlichen sollten API-Besitzer das Hinzufügen von allows ref struct zu Typparametern vermeiden, wenn sich der besitzereigene Typ/Member in Zukunft so ändern kann, dass er mit allows ref structnicht mehr kompatibel ist. Zum Beispiel:
- Eine
abstractMethode, die sich später in einevirtual-Methode ändern kann - Ein
abstractTyp, der später Implementierungen hinzufügen kann
In solchen Fällen sollte ein API-Autor vorsichtig beim Hinzufügen von allows ref struct sein, es sei denn, man ist sicher, dass die Entwicklung des Typs/Members T nicht in einer Weise nutzt, die die Regeln von ref struct verletzt.
Das Entfernen der allows ref struct Anti-Beschränkung ist immer eine fehlerhafte Änderung: im Quellcode und im Binärcode.
Standardschnittstellenmethoden
API-Autoren müssen beachten, dass das Hinzufügen von DIMS ref struct Implementoren unterbricht, bis sie neu kompiliert werden. Dies ähnelt dem bestehenden DIM-Verhalten, bei dem das Hinzufügen eines DIM zu einer Schnittstelle dazu führt, dass vorhandene Implementierungen unterbrochen werden, bis diese neu kompiliert werden. Das bedeutet, dass API-Autoren die Wahrscheinlichkeit von ref struct Implementierungen beim Hinzufügen von DIMs berücksichtigen müssen.
Es gibt drei Codekomponenten, die zum Erstellen dieser Situation erforderlich sind:
interface I1
{
// 1. The addition of a DIM method to an _existing_ interface
void M() { }
}
// 2. A ref struct implementing the interface but not explicitly defining the DIM
// method
ref struct S : I1 { }
// 3. The invocation of the DIM method in a generic method that has the `allows ref struct`
// anti-constraint
void M<T>(T p)
where T : allows ref struct, I1
{
p.M();
}
Alle drei dieser Komponenten sind erforderlich, um dieses problem zu erstellen. Außerdem müssen mindestens (1) und (2) in verschiedenen Assemblies sein. Wenn sie sich in derselben Assembly befinden, tritt ein Kompilierungsfehler auf.
UnscopedRef
Das Hinzufügen oder Entfernen von [UnscopedRef] aus interface Mitgliedern ist eine fehlerhafte Änderung des Quellcodes (und erstellt möglicherweise Laufzeitprobleme). Das Attribut sollte beim Definieren eines Schnittstellenelements angewendet und später nicht hinzugefügt oder entfernt werden.
Span Span<<T>>
Diese Kombination von Features lässt keine Konstrukte wie Span<Span<T>>zu. Dies wird etwas klarer gemacht, indem man sich die Definition von Span<T>ansieht:
readonly ref struct Span<T>
{
public readonly ref T _data;
public readonly int _length;
public Span(T[] array) { ... }
public static implicit operator Span<T>(T[]? array) { }
public static implicit operator Span<T>(ArraySegment<T> segment) { }
}
Wenn diese Typdefinition allows ref struct einschließen würde, müssten alle T Instanzen in der Definition so behandelt werden, als wären sie potenziell ein ref struct Typ. Das stellt zwei Klassen von Problemen dar.
Die erste ist für APIs wie Span(T[] array) und die impliziten Operatoren, bei denen T kein ref structsein kann: Sie werden entweder als Arrayelemente oder als generische Parameter verwendet, die nicht allows ref structwerden können. Es gibt eine Handvoll öffentliche APIs für Span<T>, die T verwenden, die nicht mit einem ref structkompatibel sind. Dies sind öffentliche APIs, die nicht gelöscht werden können und daher von der Sprache rationalisiert werden müssen. Der wahrscheinlichste Weg ist, dass der Compiler den Sonderfall Span<T> behandelt und einen Fehlercode an eine dieser APIs ausgibt, wenn das Argument für Tpotentiell ein ref struct ist.
Das Zweite ist, dass die Sprache keine ref Felder unterstützt, die ref structsind. Es gibt einen Entwurfsvorschlag, um dieses Feature zuzulassen. Es ist unklar, ob dies in die Sprache akzeptiert wird oder ob es ausdrucksvoll genug ist, um den vollständigen Satz von Szenarien um Span<T>zu behandeln.
Beide Fragen liegen außerhalb des Geltungsbereichs dieses Vorschlags.
UnscopedRef-Implementierungslogik
Die Logik hinter den [UnscopedRef] Regeln für die Schnittstellenimplementierung ist am einfachsten zu verstehen, wenn der this Parameter als explizites Argument anstelle impliziter Argumente für die Methoden visualisiert wird. Betrachten Sie beispielsweise die folgende struct, in der this als impliziter Parameter visualisiert wird (ähnlich wie Python es behandelt):
struct S
{
public void M(scoped ref S this) { }
}
Das [UnscopedRef] auf einem Mitglied der Schnittstelle gibt an, dass this für die Lebensdauer am Aufrufort scoped fehlt. Das Weglassen von [UnscopedRef] auf dem implementierenden Mitglied bietet effektiv die Möglichkeit, dass ein Parameter, der ref T ist, durch einen Parameter, der scoped ref T ist, implementiert wird. Die Sprache lässt dies bereits zu:
interface I1
{
void M(ref Span<char> span);
}
struct S : I1
{
public void M(scoped ref Span<char> span) { }
}
Verwandte Elemente
Verwandte Elemente:
- https://github.com/dotnet/csharplang/issues/7608
- https://github.com/dotnet/csharplang/pull/7555
- https://github.com/dotnet/runtime/blob/main/docs/design/features/byreflike-generics.md
- https://github.com/dotnet/runtime/pull/67783
- https://github.com/dotnet/runtime/issues/27229#issuecomment-1537274804
- https://github.com/dotnet/runtime/issues/68002
C# feature specifications