Freigeben über


Erstklassige Span-Typen

Hinweis

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 relevanten Sprachentwurfsbesprechungen (LDM)-Notizen 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/8714

Zusammenfassung

Wir stellen erstklassige Unterstützung für Span<T> und ReadOnlySpan<T> in der Sprache vor, einschließlich neuer impliziter Konvertierungstypen und betrachten sie an weiteren Stellen, was eine natürlichere Programmierung mit diesen integralen Typen ermöglicht.

Motivation

Seit ihrer Einführung in C# 7.2 haben sich Span<T> und ReadOnlySpan<T> auf viele entscheidende Weisen in die Sprache und die Basisklassenbibliothek (BCL) integriert. Dies eignet sich hervorragend für Entwickler, da ihre Einführung die Leistung verbessert, ohne die Entwicklersicherheit zu kosten. Die Sprache hat diese Typen jedoch in einigen wichtigen Punkten auf Abstand gehalten, was es schwierig macht, die Absicht von APIs auszudrücken und zu einer erheblichen Verdoppelung der Oberfläche für neue APIs führt. Beispielsweise hat die BCL eine Reihe neuer Tensor-Grundtyp-APIs in .NET 9 hinzugefügt, aber diese APIs werden alle auf ReadOnlySpan<T> angeboten. C# erkennt die Beziehung zwischen ReadOnlySpan<T>, Span<T> und T[] nicht. Obwohl also benutzerdefinierte Konvertierungen zwischen diesen Typen vorhanden sind, können diese nicht für Empfänger von Erweiterungsmethoden verwendet werden, können nicht mit anderen benutzerdefinierten Konvertierungen kombiniert werden und helfen nicht bei allen Szenarien mit generischer Typinferenz. Benutzer müssen explizite Konvertierungen oder Typargumente verwenden. Das bedeutet, dass die IDE-Tools die Benutzer nicht zur Verwendung dieser APIs anleiten, da nichts der IDE anzeigt, dass es gültig ist, diese Typen nach der Konvertierung zu übergeben. Um maximale Benutzerfreundlichkeit für diesen API-Stil bereitzustellen, muss die BCL einen ganzen Satz von Span<T>- und T[]-Überladungen definieren, was eine Menge doppelter Funktionsoberfläche ist, die ohne wirklichen Nutzen beibehalten werden muss. Dieser Vorschlag zielt darauf ab, das Problem zu beheben, indem die Sprache diese Typen und Konvertierungen besser erkennt.

Zum Beispiel kann die BCL nur eine Überladung eines MemoryExtensions -Helpers wie:

int[] arr = [1, 2, 3];
Console.WriteLine(
    arr.StartsWith(1) // CS8773 in C# 13, permitted with this proposal
    );

public static class MemoryExtensions
{
    public static bool StartsWith<T>(this ReadOnlySpan<T> span, T value) where T : IEquatable<T> => span.Length != 0 && EqualityComparer<T>.Default.Equals(span[0], value);
}

Bisher wären Span- und Arrayüberladungen erforderlich, um die Erweiterungsmethode für Span/Array-typierte Variablen nutzbar zu machen, da benutzerdefinierte Konvertierungen (die zwischen Span/Array/ReadOnlySpan vorhanden sind) für Erweiterungsempfänger nicht berücksichtigt werden.

Detailliertes Design

Die Änderungen dieses Vorschlags werden an LangVersion >= 14 gebunden sein.

Spanne Konvertierungen

Wir fügen einen neuen Typ impliziter Konvertierung, eine implizite Bereichs-Konvertierung, der Liste in §10.2.1 hinzu. Diese Konvertierung ist eine Konvertierung vom Typ und ist wie folgt definiert:


Eine implizite Span-Konvertierung erlaubt, array_types, System.Span<T>, System.ReadOnlySpan<T> und string wie folgt ineinander zu konvertieren.

  • Von einem eindimensionalen array_type mit Elementtyp Ei zu System.Span<Ei>
  • Von einem eindimensionalen array_type mit Elementtyp Ei nach System.ReadOnlySpan<Ui>, vorausgesetzt, dass Ei kovarianz-konvertierbar ist (§18.2.3.3) zu Ui.
  • Von System.Span<Ti> bis System.ReadOnlySpan<Ui>, vorausgesetzt, dass Ti kovarianzkonvertierbar (§18.2.3.3) zu Ui
  • Von System.ReadOnlySpan<Ti> bis System.ReadOnlySpan<Ui>, vorausgesetzt, dass Ti kovarianzkonvertierbar (§18.2.3.3) zu Ui
  • Von string bis System.ReadOnlySpan<char>

Alle Span/ReadOnlySpan-Typen werden für die Konvertierung als anwendbar betrachtet, wenn sie ref structs sind und mit ihrem vollqualifizierten Namen übereinstimmen (LDM 2024-06-24).

Außerdem fügen wir die implizite Span-Konvertierung zur Liste der impliziten Standardkonvertierungen hinzu (§10.4.2). Dadurch kann die Überladungsauflösung sie bei der Argumentauflösung berücksichtigen, wie in dem zuvor verlinkten API-Vorschlag.

Die expliziten Spannenumwandlungen sind die folgenden:

  • Alle impliziten Span-Konvertierungen.
  • Von einem array_type mit Elementtyp Ti zu System.Span<Ui> oder System.ReadOnlySpan<Ui>, vorausgesetzt, dass eine explizite Verweiskonvertierung von Ti zu Ui existiert.

Es gibt keine standardmäßige explizite Span-Konvertierung im Gegensatz zu anderen expliziten Standardkonvertierungen (§10.4.3), die immer aufgrund der entgegengesetzten impliziten Standardkonvertierung vorhanden sind.

Benutzerdefinierte Konvertierungen

Benutzerdefinierte Konvertierungen werden beim Konvertieren zwischen Typen, für die eine implizite oder eine explizite Span-Konvertierung vorhanden ist, nicht berücksichtigt.

Die impliziten Span-Konvertierungen sind von der Regel ausgenommen, dass es nicht möglich ist, einen benutzerdefinierten Operator zwischen Typen zu definieren, für die eine nicht benutzerdefinierte Konvertierung vorhanden ist (§10.5.2 Zulässige benutzerdefinierte Konvertierungen). Dies ist erforderlich, damit die BCL die bestehenden Span-Konvertierungsoperatoren auch nach dem Wechsel zu C# 14 weiter definieren kann (sie werden weiterhin für niedrigere LangVersionen benötigt und auch weil diese Operatoren im Codegen der neuen Standard-Span-Konvertierungen verwendet werden). Es kann jedoch als Implementierungsdetail betrachtet werden (Codegen und niedrigere LangVersions sind nicht Teil der Spezifikation) und Roslyn verletzt diesen Teil der Spezifikation trotzdem (diese spezielle Regel über benutzerdefinierte Konvertierungen wird nicht erzwungen).

Erweiterungsempfänger

Außerdem fügen wir die implizite Span-Konvertierung zur Liste der zulässigen impliziten Konvertierungen für den ersten Parameter einer Erweiterungsmethode hinzu, wenn die Anwendbarkeit (12.8.9.3) bestimmt wird (Änderung in Fettdruck).

Eine Erweiterungsmethode Cᵢ.Mₑ ist berechtigt, wenn:

  • Cᵢ ist eine nicht generische, nicht geschachtelte Klasse
  • Der Name von Mₑ lautet identifier
  • Mₑ ist zugriffsfrei und anwendbar, wenn es auf die Argumente als statische Methode angewendet wird, wie oben gezeigt.
  • Es existiert eine implizite Identität, Referenz oder Boxing, Boxing oder span Umwandlung von expr in den Typ des ersten Parameters von Mₑ. Span-Konvertierung wird nicht berücksichtigt, wenn eine Überlastauflösung für eine Methodengruppen-Konvertierung durchgeführt wird.

Beachten Sie, dass die implizite Span-Konvertierung für erweiterungsempfänger in Methodengruppenkonvertierungen (LDM 2024-07-15) nicht berücksichtigt wird, wodurch der folgende Code weiterhin funktioniert, anstatt zu einem Kompilierungszeitfehler CS1113: Extension method 'E.M<int>(Span<int>, int)' defined on value type 'Span<int>' cannot be used to create delegateszu führen:

using System;
using System.Collections.Generic;
Action<int> a = new int[0].M; // binds to M<int>(IEnumerable<int>, int)
static class E
{
    public static void M<T>(this Span<T> s, T x) => Console.Write(1);
    public static void M<T>(this IEnumerable<T> e, T x) => Console.Write(2);
}

Als mögliche zukünftige Arbeit könnten wir in Erwägung ziehen, diese Bedingung zu entfernen, die besagt, dass die Span-Konvertierung für den Erweiterungsempfänger in Methodengruppen-Konvertierungen nicht berücksichtigt wird, und stattdessen Änderungen zu implementieren, so dass das Szenario wie das obige am Ende erfolgreich die Span Überladung aufruft:

  • Der Compiler könnte einen Thunk ausgeben, der das Array als Empfänger übernimmt und die Span-Konvertierung intern ausführt (ähnlich wie der Benutzer, der den Delegaten x => new int[0].M(x) manuell erstellt).
  • Wertdelegierte könnten, wenn sie implementiert sind, Span direkt als Empfänger nehmen.

Abweichung

Das Ziel des Varianzabschnitts in der impliziten Span-Konvertierung ist die Replikation einiger Kovarianz für System.ReadOnlySpan<T>. Laufzeitänderungen wären erforderlich, um die Varianz durch Generika hier vollständig zu implementieren (siehe .. /csharp-13.0/ref-struct-interfaces.md für die Verwendung von ref struct Typen in Generika), aber wir können eine begrenzte Anzahl von Kovarianzen durch Verwendung einer vorgeschlagenen .NET 9-API zulassen: https://github.com/dotnet/runtime/issues/96952. Dadurch kann die Sprache System.ReadOnlySpan<T> so behandeln, als ob T als out T in bestimmten Szenarien deklariert wäre. Wir untersuchen diese Variantenumwandlung jedoch nicht gründlich durch alle Varianzszenarien und fügen sie nicht zur Definition von varianzkonvertibel in §18.2.3.3 hinzu. Wenn wir in Zukunft die Laufzeit ändern, um die Abweichung hier besser zu verstehen, können wir die geringfügige Änderung vornehmen, um sie in der Sprache vollständig zu erkennen.

Muster

Beachten Sie, dass bei der Verwendung von ref struct als Typ in einem beliebigen Muster nur Identitätsumwandlungen zulässig sind.

class C<T> where T : allows ref struct
{
    void M1(T t) { if (t is T x) { } } // ok (T is T)
    void M2(R r) { if (r is R x) { } } // ok (R is R)
    void M3(T t) { if (t is R x) { } } // error (T is R)
    void M4(R r) { if (r is T x) { } } // error (R is T)
}
ref struct R { }

Aus der Spezifikation des is-type-Operators (§12.12.12.1):

Das Ergebnis des Vorgangs E is T [...] ist ein boolescher Wert, der angibt, ob E nicht null ist und erfolgreich durch eine Verweiskonvertierung, eine Boxing-Konvertierung, eine Unboxing-Konvertierung, eine Einhüllungskonvertierung oder eine Auspackkonvertierung in den Typ T konvertiert werden kann.

[...]

Wenn T ein nicht-nullbarer Wertetyp ist, ist das Ergebnis true, wenn D und T der gleiche Typ sind.

Dieses Verhalten ändert sich nicht mit diesem Feature, daher ist es nicht möglich, Muster für Span/ReadOnlySpanzu schreiben, obwohl ähnliche Muster für Arrays (einschließlich Varianz) möglich sind:

using System;

M1<object[]>(["0"]); // prints
M1<string[]>(["1"]); // prints

void M1<T>(T t)
{
    if (t is object[] r) Console.WriteLine(r[0]); // ok
}

void M2<T>(T t) where T : allows ref struct
{
    if (t is ReadOnlySpan<object> r) Console.WriteLine(r[0]); // error
}

Codeerzeugung

Die Konvertierungen sind immer vorhanden, unabhängig davon, ob Laufzeithelfer vorhanden sind, die zu ihrer Implementierung verwendet wurden (LDM 2024-05-13). Wenn die Helfer nicht vorhanden sind, führt der Versuch, die Konvertierung zu verwenden, zu einem Kompilierfehler, dass ein vom Compiler erforderliches Element fehlt.

Der Compiler erwartet, dass die folgenden Hilfsprogramme oder Entsprechungen zum Implementieren der Konvertierungen verwendet werden:

Umwandlung Helfer
Array zu Span static implicit operator Span<T>(T[]) (definiert in Span<T>)
Array zu ReadOnlySpan static implicit operator ReadOnlySpan<T>(T[]) (definiert in ReadOnlySpan<T>)
Span zu ReadOnlySpan static implicit operator ReadOnlySpan<T>(Span<T>) (definiert in Span<T>) und static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
ReadOnlySpan zu ReadOnlySpan static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
string zu ReadOnlySpan static ReadOnlySpan<char> MemoryExtensions.AsSpan(string)

Beachten Sie, dass MemoryExtensions.AsSpan anstelle des entsprechenden impliziten Operators verwendet wird, der auf string definiert ist. Dies bedeutet, dass sich der Codegen zwischen LangVersions unterscheidet (der implizite Operator wird in C# 13 verwendet; die statische Methode AsSpan wird in C# 14 verwendet). Andererseits kann die Konvertierung auf .NET Framework ausgegeben werden (die AsSpan Methode ist dort vorhanden, während der string Operator nicht).

Die explizite Konvertierung von Array in (ReadOnly)Span konvertiert zunächst explizit vom Quell-Array in ein Array mit dem Typ des Zielelements und dann in (ReadOnly)Span über denselben Helfer, den auch eine implizite Konvertierung verwenden würde, d.h. das entsprechende op_Implicit(T[]).

Bessere Konvertierung eines Ausdrucks

Bessere Konvertierung von Ausdruck (§12.6.4.5) wurde aktualisiert, um implizite span-Konvertierungen zu bevorzugen. Dies basiert auf Änderungen bei der Auflösung von Überladungen in Sammlungsausdrücken.

Gegeben eine implizite Konvertierung C₁, die von einem Ausdruck E zu einem Typ T₁ konvertiert, und eine implizite Konvertierung C₂, die von einem Ausdruck E zu einem Typ T₂ konvertiert, ist C₁ eine bessere Konvertierung als C₂, wenn eine der folgenden Bedingungen erfüllt ist:

  • E ist ein Sammlungsausdruck, und C₁ ist eine bessere Sammlungsumwandlung von Ausdruck als C₂
  • E ist kein Auflistungsausdruck und eines der folgenden trifft zu:
    • E stimmt genau mit T₁ überein, und E stimmt nicht genau mit T₂
    • E entspricht weder T₁ noch T₂, und C₁ ist eine implizite Span-Konvertierung, während C₂ keine implizite Span-Konvertierung ist.
    • E stimmt genau mit beiden oder keinem von T₁ und T₂überein, beide oder keiner von C₁ und C₂ sind eine implizite Spannenumwandlung, und T₁ ist ein besseres Umwandlungsziel als T₂
  • E ist eine Methodengruppe, T₁ ist mit der einzigen besten Methode aus der Methodengruppe für die Konvertierung C₁kompatibel und T₂ ist nicht mit der einzigen besten Methode aus der Methodengruppe für die Konvertierung kompatibel. C₂

Besseres Konvertierungsziel

Besseres Konvertierungsziel (§12.6.4.7) wird aktualisiert, um ReadOnlySpan<T> gegenüber Span<T> zu bevorzugen.

Bei den beiden Typen T₁ und T₂ ist T₁ ein besseres Konvertierungsziel als T₂, wenn eine der folgenden Bedingungen erfüllt ist:

  • T₁ ist System.ReadOnlySpan<E₁>, T₂ ist System.Span<E₂>, und eine Identitätskonvertierung von E₁ zu E₂ existiert
  • T₁ ist System.ReadOnlySpan<E₁>, T₂ ist System.ReadOnlySpan<E₂>, und eine implizite Konvertierung von T₁ zu T₂ ist vorhanden, während keine implizite Konvertierung von T₂ zu T₁ vorhanden ist
  • Mindestens eine von T₁ oder T₂ ist nicht System.ReadOnlySpan<Eᵢ> und ist nicht System.Span<Eᵢ>, und eine implizite Konvertierung von T₁ zu T₂ existiert und keine implizite Konvertierung von T₂ zu T₁ existiert.
  • ...

Design-Besprechungen:

Hinweise zur Verbesserung

Die Regel Bessere Konvertierung von Ausdruck sollte sicherstellen, dass immer dann, wenn eine Überladung aufgrund der neuen Span-Konvertierungen anwendbar wird, jede potenzielle Mehrdeutigkeit mit einer anderen Überladung vermieden wird, weil die neu anwendbare Überladung bevorzugt wird.

Ohne diese Regel würde der folgende Code, der erfolgreich in C# 13 kompiliert wurde, zu einem Mehrdeutigkeitsfehler in C# 14 führen, da die neue implizite Standardkonvertierung von Array zu ReadOnlySpan auf einen Erweiterungsmethodenempfänger anwendbar ist:

using System;
using System.Collections.Generic;

var a = new int[] { 1, 2, 3 };
a.M();

static class E
{
    public static void M(this IEnumerable<int> x) { }
    public static void M(this ReadOnlySpan<int> x) { }
}

Die Regel ermöglicht auch die Einführung neuer APIs, die zuvor zu Mehrdeutigkeiten führen würden, z. B.:

using System;
using System.Collections.Generic;

C.M(new int[] { 1, 2, 3 }); // would be ambiguous before

static class C
{
    public static void M(IEnumerable<int> x) { }
    public static void M(ReadOnlySpan<int> x) { } // can be added now
}

Warnung

Da die Besserkeitsregel für die Span-Konvertierungen definiert ist, die nur in LangVersion >= 14existieren, können die API-Autoren keine neuen Überladungen hinzufügen, wenn sie weiterhin Benutzer auf LangVersion <= 13unterstützen möchten. Wenn z.B. .NET 9 BCL solche Überladungen einführt, erhalten Benutzer, die auf net9.0 TFM aktualisieren, aber auf der niedrigeren LangVersion bleiben, Mehrdeutigkeitsfehler für bestehenden Code. Siehe auch eine offene Frage weiter unten.

Typinferenz

Wir aktualisieren den Abschnitt über Typinferenzen in der Spezifikation wie folgt (Änderungen in bold).

12.6.3.9 Exakte Ableitungen

Eine genaue Ableitungaus einem Typ Uin einen Typ V erfolgt wie folgt:

  • Wenn V einer der nicht korrigiertenXᵢ ist, wird U dem Satz exakter Grenzen für Xᵢ hinzugefügt.
  • Alternativ werden V₁...Vₑ und U₁...Uₑ bestimmt, indem überprüft wird, ob einer der folgenden Fälle zutrifft.
    • V ist ein Arraytyp V₁[...] und U ist ein Arraytyp U₁[...] desselben Rangs.
    • V ist ein Span<V₁> und U ist ein Arraytyp U₁[] oder ein Span<U₁>
    • V ist ein ReadOnlySpan<V₁> und U ist entweder ein Arraytyp U₁[] oder ein Span<U₁> oder ein ReadOnlySpan<U₁>
    • V ist der Typ V₁? und U ist der Typ U₁
    • V ist ein konstruierter Typ C<V₁...Vₑ> und U ist ein konstruierter Typ C<U₁...Uₑ>
      Wenn eines dieser Fälle zutrifft, wird eine genaue Ableitung von jedem Uᵢ auf die entsprechende Vᵢ vorgenommen.
  • Andernfalls werden keine Ableitungen vorgenommen.

12.6.3.10 Untergrenzableitungen

Eine Untergrenzableitung von einem Typ Uin einen Typ V wird wie folgt vorgenommen:

  • Wenn V einer der nicht korrigiertenXᵢ ist, wird U dem Satz unterer Grenzen für Xᵢ hinzugefügt.
  • Andernfalls, wenn V der Typ V₁? und U der Typ U₁? sind, wird eine Untergrenzenableitung von U₁ in V₁ vorgenommen.
  • Alternativ werden U₁...Uₑ und V₁...Vₑ bestimmt, indem überprüft wird, ob einer der folgenden Fälle zutrifft.
    • V ist ein Arraytyp V₁[...] und U ein Arraytyp U₁[...]desselben Rangs.
    • V ist ein Span<V₁> und U ist ein Arraytyp U₁[] oder ein Span<U₁>
    • V ist ein ReadOnlySpan<V₁> und U ist entweder ein Arraytyp U₁[] oder ein Span<U₁> oder ein ReadOnlySpan<U₁>
    • V ist einer von IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> oder IList<V₁> und U ist ein eindimensionales Array vom Typ U₁[]
    • V ist ein konstruierter class-, struct-, interface- oder delegate-Typ C<V₁...Vₑ>, und es gibt einen eindeutigen Typ C<U₁...Uₑ>, sodass für das U-Element (oder, wenn U von Typ parameter ist, seine effektive Basisklasse oder ein Element seines effektiven Schnittstellensatzes) Folgendes gilt: Es ist identisch mit, erbt (inherits) (direkt oder indirekt) von oder implementiert (direkt oder indirekt) C<U₁...Uₑ>.
    • (Die Einschränkung der „Eindeutigkeit“ bedeutet, dass im Fall der Schnittstelle C<T>{} class U: C<X>, C<Y>{} keine Ableitung von U nach C<T> vorgenommen wird, da U₁ auch X oder Y sein könnte).
      Wenn einer dieser Fälle zutrifft, wird wie folgt ein Rückschluss von jedem Uᵢ-Element in das entsprechende Vᵢ-Element durchgeführt:
    • Wenn Uᵢ nicht als Referenztyp bekannt ist, wird eine exakte Ableitung vorgenommen.
    • Andernfalls, wenn U ein Array-Typ ist, wird eine Untergrenzen-Inferenz gemachtDie Inferenz hängt vom Typ von Vab:
      • Wenn V ein Span<Vᵢ> ist, wird eine genaue Ableitung vorgenommen
      • Wenn V ein Arraytyp oder ein ReadOnlySpan<Vᵢ> ist, wird eine Untergrenzenableitung vorgenommen.
    • Andernfalls, wenn U ein Span<Uᵢ> ist, hängt die Inferenz vom Typ von V ab:
      • Wenn V ein Span<Vᵢ> ist, wird eine genaue Ableitung vorgenommen
      • Wenn V ein ReadOnlySpan<Vᵢ> ist, wird eine untere gebundene Ableitung vorgenommen.
    • Andernfalls, wenn U ein ReadOnlySpan<Uᵢ> ist und V ein ReadOnlySpan<Vᵢ> ist, wird eine untere Schrankenableitung vorgenommen:
    • Andernfalls, wenn VC<V₁...Vₑ> ist, hängt die Ableitung von dem i-th-Typparameter von C ab:
      • Wenn der Typparameter kovariant ist, erfolgt eine Untergrenzenableitung.
      • Wenn er kontravariant ist, wird eine Obergrenzenableitung vorgenommen.
      • Wenn er invariant ist, wird eine genaue Ableitung vorgenommen.
  • Andernfalls werden keine Ableitungen vorgenommen.

Es gibt keine Regeln für die obergebundene Ableitung , da es nicht möglich wäre, sie zu treffen. Die Typinferenz beginnt nie mit einer Obergrenze, sondern muss durch eine Untergrenze und einen kontravarianten Typparameter gehen. Aufgrund der Regel "wenn Uᵢ kein Bezugstyp bekannt ist, wird eine genaue Ableitung vorgenommen", konnte das Quelltypargument nicht sein Span/ReadOnlySpan (diese können nicht referenziert werden). Die Inferenz für die obere Grenze der Spanne würde jedoch nur gelten, wenn der Quelltyp ein Span/ReadOnlySpanwäre, da es Regeln wie:

  • U ist ein Span<U₁> und V ist ein Arraytyp V₁[] oder ein Span<V₁>
  • U ist ein ReadOnlySpan<U₁> und V ist entweder ein Arraytyp V₁[] oder ein Span<V₁> oder ein ReadOnlySpan<V₁>

Bahnbrechende Änderungen

Wie jeder Vorschlag, der Umwandlungen bestehender Szenarien verändert, führt dieser Vorschlag einige neue bahnbrechende Änderungen ein. Hier einige Beispiele:

Aufruf von Reverse für ein Array

Das Aufrufen x.Reverse(), bei dem x eine Instanz des Typs T[] ist, würde zuvor an IEnumerable<T> Enumerable.Reverse<T>(this IEnumerable<T>) binden, während es jetzt an void MemoryExtensions.Reverse<T>(this Span<T>) bindet. Leider sind diese APIs nicht kompatibel (letzteres führt die Umkehrung an Ort und Stelle durch und gibt void zurück).

.NET 10 entschärft dies durch Hinzufügen einer arrayspezifischen Überladung IEnumerable<T> Reverse<T>(this T[]), siehe https://github.com/dotnet/runtime/issues/107723.

void M(int[] a)
{
    foreach (var x in a.Reverse()) { } // fine previously, an error now (`Reverse` returns `void`)
    foreach (var x in Enumerable.Reverse(a)) { } // workaround
}

Siehe auch:

Design-Besprechung: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#reverse

Zweideutigkeiten

Die folgenden Beispiele schlugen zuvor bei der Typinferenz für die Überladung Span fehl, aber jetzt ist die Typinferenz von array zu Span erfolgreich, daher sind sie mehrdeutig. Um dies zu umgehen, können Benutzer verwenden .AsSpan() oder API-Autoren verwenden OverloadResolutionPriorityAttribute.

var x = new long[] { 1 };
Assert.Equal([2], x); // previously Assert.Equal<T>(T[], T[]), now ambiguous with Assert.Equal<T>(ReadOnlySpan<T>, Span<T>)
Assert.Equal([2], x.AsSpan()); // workaround
var x = new int[] { 1, 2 };
var s = new ArraySegment<int>(x, 1, 1);
Assert.Equal(x, s); // previously Assert.Equal<T>(T, T), now ambiguous with Assert.Equal<T>(Span<T>, Span<T>)
Assert.Equal(x.AsSpan(), s); // workaround

xUnit fügt weitere Überladungen hinzu, um dies zu vermeiden: https://github.com/xunit/xunit/discussions/3021.

Design-Besprechung: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#new-ambiguities

Kovariante Arrays

Überladungen mit IEnumerable<T> funktionierten bei kovarianten Arrays, aber Überladungen mit Span<T> (die wir jetzt bevorzugen) funktionieren nicht, da die Span-Konvertierung bei kovarianten Arrays eine ArrayTypeMismatchException auslöst. Man könnte sagen, dass die Span<T> Überladung nicht existieren sollte; stattdessen sollte ReadOnlySpan<T> verwendet werden. Um dies zu umgehen, können Benutzer .AsEnumerable()verwenden, oder API-Autoren können OverloadResolutionPriorityAttribute verwenden oder eine ReadOnlySpan<T> Überladung hinzufügen, die aufgrund der Besserungsregelbevorzugt wird.

string[] s = new[] { "a" };
object[] o = s;

C.R(o); // wrote 1 previously, now crashes in Span<T> constructor with ArrayTypeMismatchException
C.R(o.AsEnumerable()); // workaround

static class C
{
    public static void R<T>(IEnumerable<T> e) => Console.Write(1);
    public static void R<T>(Span<T> s) => Console.Write(2);
    // another workaround:
    public static void R<T>(ReadOnlySpan<T> s) => Console.Write(3);
}

Design-Besprechung: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#covariant-arrays

ReadOnlySpan gegenüber Span bevorzugen

Die Besserungsregel bewirkt, dass ReadOnlySpan-Überladungen gegenüber Span-Überladungen bevorzugt werden, um ArrayTypeMismatchExceptions in kovarianten Array-Szenarienzu vermeiden. Dies kann in einigen Szenarien zu Kompilierungsumbrüchen führen, z. B. wenn sich die Überladungen je nach Rückgabetyp unterscheiden:

double[] x = new double[0];
Span<ulong> y = MemoryMarshal.Cast<double, ulong>(x); // previously worked, now a compilation error (returns ReadOnlySpan, not Span)
Span<ulong> z = MemoryMarshal.Cast<double, ulong>(x.AsSpan()); // workaround

static class MemoryMarshal
{
    public static ReadOnlySpan<TTo> Cast<TFrom, TTo>(ReadOnlySpan<TFrom> span) => default;
    public static Span<TTo> Cast<TFrom, TTo>(Span<TFrom> span) => default;
}

Siehe https://github.com/dotnet/roslyn/issues/76443.

Ausdrucksbaumstrukturen

Überladungen mit Spannen wie MemoryExtensions.Contains werden gegenüber klassischen Überladungen wie Enumerable.Containsbevorzugt, auch innerhalb von Ausdrucksbäumen - aber ref structs werden von der Interpreter-Engine nicht unterstützt:

Expression<Func<int[], int, bool>> exp = (array, num) => array.Contains(num);
exp.Compile(preferInterpretation: true); // fails at runtime in C# 14

Expression<Func<int[], int, bool>> exp2 = (array, num) => Enumerable.Contains(array, num); // workaround
exp2.Compile(preferInterpretation: true); // ok

Ebenso müssen Übersetzungsmodule wie LINQ-to-SQL darauf reagieren, wenn ihre Baumbesucher Enumerable.Contains erwarten, weil sie stattdessen MemoryExtensions.Contains vorfinden werden.

Siehe auch:

Design-Besprechungen:

Benutzerdefinierte Konvertierungen durch Vererbung

Durch Hinzufügen impliziter Span-Konvertierungen zur Liste der impliziten Standardkonvertierungen können wir das Verhalten möglicherweise ändern, wenn benutzerdefinierte Konvertierungen an einer Typhierarchie beteiligt sind. Dieses Beispiel zeigt diese Änderung im Vergleich zu einem Integer-Szenario, das sich bereits so verhält wie das neue C# 14-Verhalten.

Span<string> span = [];
var d = new Derived();
d.M(span); // Base today, Derived tomorrow
int i = 1;
d.M(i); // Derived today, demonstrates new behavior

class Base
{
    public void M(Span<string> s)
    {
        Console.WriteLine("Base");
    }

    public void M(int i)
    {
        Console.WriteLine("Base");
    }
}

class Derived : Base
{
    public static implicit operator Derived(ReadOnlySpan<string> r) => new Derived();
    public static implicit operator Derived(long l) => new Derived();

    public void M(Derived s)
    {
        Console.WriteLine("Derived");
    }
}

Weitere Informationen: https://github.com/dotnet/roslyn/issues/78314

Nachschlagen von Erweiterungsmethoden

Durch das Zulassen von impliziten Span-Konvertierungen bei der Suche nach Erweiterungsmethoden können wir potenziell ändern, welche Erweiterungsmethode durch Überladungsauflösung ermittelt wird.

namespace N1
{
    using N2;

    public class C
    {
        public static void M()
        {
            Span<string> span = new string[0];
            span.Test(); // Prints N2 today, N1 tomorrow
        }
    }

    public static class N1Ext
    {
        public static void Test(this ReadOnlySpan<string> span)
        {
            Console.WriteLine("N1");
        }
    }
}

namespace N2
{
    public static class N2Ext
    {
        public static void Test(this Span<string> span)
        {
            Console.WriteLine("N2");
        }
    }
}

Offene Fragen

Uneingeschränkte Besserkeitsregel

Sollten wir die Besserheitsregel bedingungslos auf LangVersion anwenden? Dadurch könnten API-Autoren neue Span-APIs hinzufügen, bei denen IEnumerable-Entsprechungen vorhanden sind, ohne Benutzer in älteren LangVersions oder anderen Compilern oder Sprachen (z. B. VB) zu unterbrechen. Das würde jedoch bedeuten, dass Benutzer nach dem Aktualisieren des Toolsets ein anderes Verhalten erhalten könnten (ohne LangVersion oder TargetFramework zu ändern):

  • Compiler könnte unterschiedliche Überladungen auswählen (technisch eine bahnbrechende Änderung, aber hoffentlich würden diese Überladungen ein gleichwertiges Verhalten haben).
  • Es könnten weitere Pausen entstehen, die zu diesem Zeitpunkt noch nicht bekannt sind.

Beachten Sie, dass OverloadResolutionPriorityAttribute dies nicht vollständig gelöst werden kann, da es auch bei älteren LangVersions ignoriert wird. Es sollte jedoch möglich sein, es zu verwenden, um Mehrdeutigkeiten von VB zu vermeiden, in denen das Attribut erkannt werden sollte.

Ignorieren von weiteren benutzerdefinierten Konvertierungen

Wir haben eine Reihe von Typpaaren definiert, für die sprachdefinierte implizite und explizite Span-Konvertierungen vorhanden sind. Immer wenn eine sprachdefinierte Spannen-Konvertierung von T1 zu T2 existiert, wird jede benutzerdefinierte Konvertierung von T1 zu T2 ignoriert (unabhängig davon, ob die Spannen- und benutzerdefinierte Konvertierung implizit oder explizit sind).

Beachten Sie, dass dies alle Bedingungen enthält. Zum Beispiel gibt es keine Bereichskonvertierung von Span<object> zu ReadOnlySpan<string> (es gibt eine Konvertierung von Span<T> zu ReadOnlySpan<U>, aber es muss gelten, dass T : U). Eine benutzerdefinierte Konvertierung würde zwischen diesen Typen in Betracht gezogen werden, wenn sie existierte. Dies müsste eine spezialisierte Konvertierung wie von Span<T> zu ReadOnlySpan<string> sein, da Konvertierungsoperatoren keine generischen Parameter haben können.

Sollten benutzerdefinierte Konvertierungen auch zwischen anderen Kombinationen von Array/Span/ReadOnlySpan/String-Typen ignoriert werden, bei denen keine entsprechende sprachdefinierte Span-Konvertierung vorhanden ist? Beispielsweise, wenn es eine benutzerdefinierte Konvertierung von ReadOnlySpan<T> zu Span<T> gibt, sollten wir sie ignorieren?

Spezifische Möglichkeiten, die Sie berücksichtigen sollten:

  1. Wann immer eine Span-Konvertierung von T1 zu T2 vorhanden ist, ignorieren Sie jede benutzerdefinierte Konvertierung von T1 zu T2 oder von T2 zu T1.

  2. Benutzerdefinierte Konvertierungen werden beim Konvertieren zwischen verschiedenen Datentypen nicht berücksichtigt.

    • jede eindimensionale array_type und System.Span<T>/System.ReadOnlySpan<T>,
    • beliebige Kombination von System.Span<T>/System.ReadOnlySpan<T>,
    • string und System.ReadOnlySpan<char>.
  3. Wie oben, aber ersetzen Sie den letzten Aufzählungspunkt durch:
    • string und System.Span<char>/System.ReadOnlySpan<char>.
  4. Wie oben, aber ersetzen Sie den letzten Aufzählungspunkt durch:
    • string und System.Span<T>/System.ReadOnlySpan<T>.

Technisch lässt die Spezifikation nicht zu, dass einige dieser benutzerdefinierten Konvertierungen sogar definiert werden: Es ist nicht möglich, einen benutzerdefinierten Operator zwischen Typen zu definieren, für die eine nicht benutzerdefinierte Konvertierung vorhanden ist (§10.5.2). Aber Roslyn verletzt absichtlich diesen Teil der Spezifikation. Und einige Konvertierungen wie zwischen Span und string sind trotzdem zulässig (keine sprachdefinierte Konvertierung zwischen diesen Typen vorhanden).

Dennoch könnten wir alternativ zur ignorierung der Konvertierungen verhindern , dass sie überhaupt definiert werden und vielleicht die Spezifikationsverletzung zumindest für diese neuen Span-Konvertierungen aufheben, d.h. Roslyn so ändern, dass es einen Kompilierzeitfehler meldet, wenn diese Konvertierungen definiert sind (wahrscheinlich mit Ausnahme derer, die bereits in der BCL definiert sind).

Alternativen

Behalten Sie die Dinge so, wie sie sind.