Bereiche
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 werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.
Weitere Informationen zum Prozess für die Aufnahme von Funktions-Speclets in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.
Champion Issue: https://github.com/dotnet/csharplang/issues/185
Zusammenfassung
Bei diesem Feature geht es um die Bereitstellung von zwei neuen Operatoren, die das Erstellen von System.Index
- und System.Range
-Objekten ermöglichen, um diese zur Laufzeit zum Indizieren/Segmentieren von Sammlungen zu verwenden.
Übersicht
Bekannte Typen und Mitglieder
Um die neuen syntaktischen Formen für System.Index
und System.Range
zu verwenden, könnten – abhängig von den gewählten syntaktischen Formen – neue bekannte Typen und Member erforderlich werden.
Um den Operator "Caret" (^
) zu verwenden, ist Folgendes erforderlich:
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
Um den Typ System.Index
als Argument in einem Array-Elementzugriff zu verwenden, ist das folgende Mitglied erforderlich:
int System.Index.GetOffset(int length);
Die ..
-Syntax für System.Range
erfordert den System.Range
-Typ und eines oder mehrere der folgenden Member:
namespace System
{
public readonly struct Range
{
public Range(System.Index start, System.Index end);
public static Range StartAt(System.Index start);
public static Range EndAt(System.Index end);
public static Range All { get; }
}
}
Die ..
-Syntax ermöglicht, dass entweder eines, beide oder keines der Argumente fehlen kann. Unabhängig von der Anzahl der Argumente ist der Range
Konstruktor immer ausreichend für die Verwendung der Range
Syntax. Wenn jedoch eines der anderen Member vorhanden ist und mindestens eines der ..
-Argumente fehlt, kann das entsprechende Member ersetzt werden.
Schließlich muss für einen Wert vom Typ System.Range
, der in einem Ausdruck für den Zugriff auf ein Arrayelement verwendet werden soll, das folgende Element vorhanden sein:
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}
System.Index
C# bietet keine Möglichkeit, eine Auflistung vom Ende her zu indizieren. Stattdessen verwenden die meisten Indexer das Konzept "von Anfang an" oder berechnen den Ausdruck "Länge - i". Wir führen einen neuen Indexbegriff ein, der "vom Ende her" bedeutet. Das Feature führt einen neuen unären Präfix-Operator "Caret" ein. Der einzelne Operand muss in System.Int32
konvertierbar sein. Er wird in den entsprechenden System.Index
-Factorymethoden-Aufruf eingefügt.
Wir erweitern die Grammatik für unary_expression mit folgender zusätzlicher Syntaxform:
unary_expression
: '^' unary_expression
;
Wir nennen dies den Index vom Ende-Operator. Die vordefinierten Index vom Ende-Operatoren lauten wie folgt:
System.Index operator ^(int fromEnd);
Das Verhalten dieses Operators ist nur für Eingabewerte größer als oder gleich Null definiert.
Beispiele
var array = new int[] { 1, 2, 3, 4, 5 };
var thirdItem = array[2]; // array[2]
var lastItem = array[^1]; // array[new Index(1, fromEnd: true)]
System.Range
C# hat keine syntaktische Möglichkeit, auf "Bereiche" oder "Slices" von Sammlungen zuzugreifen. In der Regel sind die Benutzer gezwungen, komplexe Strukturen zu implementieren, um auf Slices des Speichers zu filtern/operieren, oder auf LINQ-Methoden wie list.Skip(5).Take(2)
zurückzugreifen. Durch das Hinzufügen von System.Span<T>
und anderen ähnlichen Typen wird es immer wichtiger, diese Art von Vorgängen auf einer tieferen Ebene in der Sprache/zur Laufzeit zu unterstützen und die Schnittstelle zu vereinheitlichen.
In der Sprache wird ein neuer Bereichsoperator x..y
eingeführt. Es handelt sich um einen binären Infix-Operator, der zwei Ausdrücke akzeptiert. Beide Operanden können weggelassen werden (siehe Beispiele unten), und sie müssen in System.Index
konvertierbar sein. Er wird in den entsprechenden System.Range
-Factorymethoden-Aufruf eingefügt.
Wir ersetzen die C#-Grammatikregeln für multiplicative_expression durch die folgenden (um eine neue Prioritätsstufe einzuführen):
range_expression
: unary_expression
| range_expression? '..' range_expression?
;
multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;
Alle Formen des Bereichsoperators haben dieselbe Rangfolge. Diese neue Rangfolgegruppe ist niedriger als die unären Operatoren und höher als die multiplikativen arithmetischen Operatoren .
Wir nennen den ..
Operator den Bereichsoperator. Der integrierte Bereichsoperator kann grob so verstanden werden, dass er dem Aufruf eines integrierten Operators in dieser Form entspricht:
System.Range operator ..(Index start = 0, Index end = ^0);
Beispiele
var array = new int[] { 1, 2, 3, 4, 5 };
var slice1 = array[2..^3]; // array[new Range(2, new Index(3, fromEnd: true))]
var slice2 = array[..^3]; // array[Range.EndAt(new Index(3, fromEnd: true))]
var slice3 = array[2..]; // array[Range.StartAt(2)]
var slice4 = array[..]; // array[Range.All]
Außerdem sollte System.Index
über eine implizite Konvertierung von System.Int32
verfügen, um zu vermeiden, dass Ganzzahlen und Indizes in mehrdimensionalen Signaturen miteinander kombiniert werden, sodass es zu einer Überlastung kommt.
Hinzufügen von Index- und Bereichsunterstützung zu vorhandenen Bibliothekstypen
Implizite Indexunterstützung
Die Sprache stellt ein Instanz-Indexer-Mitglied mit einem einzigen Parameter vom Typ Index
für Typen bereit, die die folgenden Kriterien erfüllen:
- Der Typ ist "Countable".
- Der Typ verfügt über einen zugänglichen Instanz-Indexer, der ein einzelnes
int
als Argument akzeptiert. - Der Typ verfügt nicht über einen zugänglichen Instanzindexer, der eine
Index
als ersten Parameter akzeptiert. DerIndex
muss der einzige Parameter sein oder die übrigen Parameter müssen optional sein.
Ein Typ ist Countable, wenn er über eine Eigenschaft mit dem Namen Length
oder Count
mit einem zugreifbaren Getter und einem Rückgabetyp von int
verfügt. Die Sprache kann diese Eigenschaft verwenden, um einen Ausdruck vom Typ Index
an der Stelle des Ausdrucks in int
zu konvertieren, ohne den Typ Index
überhaupt verwenden zu müssen. Falls sowohl Length
als auch Count
vorhanden sind, wird Length
bevorzugt. Der Einfachheit halber wird der Vorschlag den Namen Length
verwenden, um Count
oder Length
darzustellen.
Bei solchen Typen verhält sich die Sprache, als ob ein Indexer-Member in der Form T this[Index index]
existiert, wobei T
der Rückgabetyp des auf int
basierenden Indexers einschließlich aller ref
-Stil-Anmerkungen ist. Das neue Member verfügt über die gleichen get
- und set
-Member mit übereinstimmenden Zugriffsrechten wie der int
-Indexer.
Der neue Indexer wird implementiert, indem das Argument vom Typ Index
in ein int
konvertiert und ein Aufruf an den auf int
basierenden Indexer gesendet wird. Für Diskussionszwecke verwenden wir das Beispiel von receiver[expr]
. Die Konversion von expr
nach int
erfolgt wie folgt:
- Wenn das Argument die Form
^expr2
hat und der Typ vonexpr2
int
ist, wird es inreceiver.Length - expr2
übersetzt. - Andernfalls wird es als
expr.GetOffset(receiver.Length)
übersetzt.
Unabhängig von der spezifischen Konversionsstrategie sollte die Reihenfolge der Auswertung wie folgt sein:
-
receiver
wird ausgewertet; -
expr
wird ausgewertet; -
length
wird bei Bedarf ausgewertet; - Der
int
-basierte Indexer wird aufgerufen.
Dies bietet Entwicklern die Möglichkeit, die Index
Funktion für bestehende Typen zu verwenden, ohne dass eine Änderung erforderlich ist. Zum Beispiel:
List<char> list = ...;
var value = list[^1];
// Gets translated to
var value = list[list.Count - 1];
Die Ausdrücke receiver
und Length
werden entsprechend überlaufen, um sicherzustellen, dass alle Nebenwirkungen nur einmal auftreten. Zum Beispiel:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int this[int index] => _array[index];
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get()[^1];
Console.WriteLine(i);
}
}
Dieser Code gibt "Get Length 3" aus.
Implizite Bereichsunterstützung
Die Sprache stellt ein Instanz-Indexer-Mitglied mit einem einzigen Parameter vom Typ Range
für Typen bereit, die die folgenden Kriterien erfüllen:
- Der Typ ist "Countable".
- Der Typ hat ein zugriffsfreies Mitglied mit dem Namen
Slice
, das zwei Parameter vom Typint
hat. - Der Typ verfügt nicht über einen Instanz-Indexer, der einen einzelnen
Range
als ersten Parameter annimmt. DerRange
muss der einzige Parameter sein oder die übrigen Parameter müssen optional sein.
Bei solchen Typen wird die Sprache gebunden, als ob ein Indexer-Member in der Form T this[Range range]
existiert, wobei T
der Rückgabetyp der Slice
-Methode einschließlich aller ref
-Stil-Anmerkungen ist. Das neue Member wird auch über eine vergleichbare Zugänglichkeit wie Slice
verfügen.
Wenn der Range
-basierte Indexer an einen Ausdruck mit dem Namen receiver
gebunden ist, wird er durch Umwandeln des Range
-Ausdrucks in zwei Werte herabgesetzt, die dann an die Slice
-Methode übergeben werden. Für Diskussionszwecke verwenden wir das Beispiel von receiver[expr]
.
Das erste Argument von Slice
wird abgerufen, indem der typisierte Bereichsausdruck wie folgt konvertiert wird:
- Wenn
expr
die Formexpr1..expr2
(wobeiexpr2
weggelassen werden kann) hat undexpr1
den Typint
hat, wird es alsexpr1
ausgegeben. - Wenn
expr
die Form^expr1..expr2
hat (wobeiexpr2
weggelassen werden kann), wird der Ausdruck alsreceiver.Length - expr1
ausgegeben. - Wenn
expr
die Form..expr2
hat (wobeiexpr2
weggelassen werden kann), wird der Ausdruck als0
ausgegeben. - Andernfalls wird er als
expr.Start.GetOffset(receiver.Length)
ausgegeben.
Dieser Wert wird bei der Berechnung des zweiten Slice
Arguments wiederverwendet. Auf diese Weise wird er als start
bezeichnet. Das zweite Argument von Slice
wird abgerufen, indem der typisierte Bereichsausdruck wie folgt konvertiert wird:
- Wenn
expr
die Formexpr1..expr2
(wobeiexpr1
weggelassen werden kann) hat undexpr2
den Typint
hat, wird es alsexpr2 - start
ausgegeben. - Wenn
expr
die Formexpr1..^expr2
hat (wobeiexpr1
weggelassen werden kann), wird der Ausdruck als(receiver.Length - expr2) - start
ausgegeben. - Wenn
expr
die Formexpr1..
hat (wobeiexpr1
weggelassen werden kann), wird der Ausdruck alsreceiver.Length - start
ausgegeben. - Andernfalls wird er als
expr.End.GetOffset(receiver.Length) - start
ausgegeben.
Unabhängig von der spezifischen Konversionsstrategie sollte die Reihenfolge der Auswertung wie folgt sein:
-
receiver
wird ausgewertet; -
expr
wird ausgewertet; -
length
wird bei Bedarf ausgewertet; - die
Slice
-Methode wird aufgerufen.
Die Ausdrücke receiver
, expr
und length
werden entsprechend überlaufen, um sicherzustellen, dass alle Nebenwirkungen nur einmal auftreten. Zum Beispiel:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int[] Slice(int start, int length) {
var slice = new int[length];
Array.Copy(_array, start, slice, 0, length);
return slice;
}
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
var array = Get()[0..2];
Console.WriteLine(array.Length);
}
}
Dieser Code gibt "Get Length 2" aus.
In der Sprache werden die folgenden bekannten Typen als Spezialfall behandelt:
-
string
: die MethodeSubstring
wird anstelle vonSlice
verwendet. -
array
: die MethodeSystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
wird anstelle vonSlice
verwendet.
Alternativen
Die neuen Operatoren (^
und ..
) sind syntaktischer Zucker. Die Funktionalität kann durch explizite Aufrufe von System.Index
- und System.Range
-Factorymethoden implementiert werden, führt aber zu viel mehr Codebausteinen, und die Erfahrung wird nicht intuitiv sein.
IL-Darstellung
Diese beiden Operatoren werden zu regulären Indexer-/Methodenaufrufen herabgestuft, ohne dass sich in nachfolgenden Schichten des Compilers etwas ändert.
Laufzeitverhalten
- Der Compiler kann Indexer für integrierte Typen wie Arrays und Zeichenfolgen optimieren und die Indizierung auf entsprechende vorhandene Methoden beschränken.
-
System.Index
wird ausgelöst, wenn es mit einem negativen Wert erstellt wird. ^0
wird nicht ausgelöst, sondern in die Länge der Sammlung/Aufzählung konvertiert, an die er übergeben wird.-
Range.All
ist semantisch äquivalent zu0..^0
und kann in diese Indizes zerlegt werden.
Überlegungen
Erkennen indizierbarer Elemente basierend auf ICollection
Die Inspiration für dieses Verhalten war ein Auflistungsinitialisierer. Anhand der Struktur eines Typs wird angegeben, dass man sich für eine Funktion entschieden hat. Im Fall von Auflistungsinitialisierern können sich Typen für das Feature entscheiden, indem die Schnittstelle IEnumerable
(nicht generisch) implementiert wird.
Dieser Vorschlag erforderte zunächst, dass Typen ICollection
implementieren, um als indizierbar zu gelten. Das erfordert allerdings eine Reihe von Sonderfällen:
ref struct
: Diese können zwar noch keine Schnittstellen implementieren, jedoch sind Typen wieSpan<T>
ideal für die Index-/Bereichsunterstützung.string
: implementiertICollection
nicht, und das Hinzufügen dieser Schnittstelle ist mit hohen Kosten verbunden.
Dies bedeutet, dass besondere Fälle für Schlüsseltypen bereits erforderlich sind. Der Spezialfall von string
ist weniger interessant, da die Sprache dies in anderen Bereichen tut (foreach
-Verringerung, Konstanten usw.). Der Spezialfall von ref struct
ist schwerwiegender, da es sich um eine spezielle Behandlung einer ganzen Klasse von Typen handelt. Sie werden als indizierbar bezeichnet, wenn sie lediglich über eine Eigenschaft mit dem Namen Count
und einem Rückgabetyp von int
verfügen.
Nach Prüfung wurde der Entwurf normalisiert, um zu sagen, dass jeder Typ, der eine Eigenschaft Count
/ Length
mit einem Rückgabetyp von int
besitzt, indexierbar ist. Dadurch werden alle Spezialfälle entfernt, auch für string
und Arrays.
Nur Anzahl erkennen
Das Erkennen der Eigenschaftennamen Count
oder Length
erschwert den Entwurf etwas. Nur einen für die Standardisierung auszuwählen, ist jedoch nicht ausreichend, da dann viele Typen ausgeschlossen würden.
- Verwenden Sie
Length
: Dadurch werden nahezu alle Sammlungen in System.Collections und Sub-Namespaces ausgeschlossen. Diese neigen dazu, vonICollection
abgeleitet zu werden und bevorzugen daherCount
statt der Länge. Count
verwenden: schließtstring
, Arrays,Span<T>
und die meistenref struct
-basierten Typen aus.
Die zusätzliche Komplikation bei der Ersterkennung von indizierbaren Typen wird durch die Vereinfachung in anderen Aspekten aufgewogen.
Auswahl von Slice als Name
Der Name Slice
wurde als de-facto-Standardname für Slice-Operationen in .NET ausgewählt. Ab netcoreapp2.1 verwenden alle Span-Formatvorlagentypen den Namen Slice
für Slicing-Vorgänge. Vor netcoreapp2.1 gibt es keine Beispiele für Slicing. Typen wie List<T>
, ArraySegment<T>
, SortedList<T>
wären ideal für das Slicing, aber das Konzept existierte nicht, als die Typen hinzugefügt wurden.
Da Slice
das einzige Beispiel war, wurde es als Name gewählt.
Indexzieltyp-Konvertierung
Eine weitere Möglichkeit zum Anzeigen der Index
-Transformation in einem Indexer-Ausdruck ist eine Zieltypkonvertierung. Anstatt zu binden, als ob ein Member im Format return_type this[Index]
vorhanden ist, weist die Sprache stattdessen eine Zieltypkonvertierung in int
zu.
Dieses Konzept könnte für alle Memberzugriffe auf Countable-Typen verallgemeinert werden. Wenn ein Ausdruck vom Typ Index
als Argument für einen Instanzmethodenaufruf verwendet wird und der Empfänger "Countable" ist, erhält der Ausdruck eine Zieltypkonvertierung zu int
. Die für diese Konvertierung anwendbaren Memberaufrufe umfassen Methoden, Indexer, Eigenschaften, Erweiterungsmethoden und so weiter. Nur Konstruktoren werden ausgeschlossen, weil sie keinen Empfänger haben.
Die Zieltypkonvertierung wird für jeden Ausdruck, der einen Typ von Index
hat, wie folgt implementiert. Lassen Sie uns zu Diskussionszwecken das Beispiel von receiver[expr]
verwenden:
- Wenn
expr
die Form^expr2
hat und der Typ vonexpr2
int
ist, wird es inreceiver.Length - expr2
übersetzt. - Andernfalls wird es als
expr.GetOffset(receiver.Length)
übersetzt.
Die Ausdrücke receiver
und Length
werden entsprechend überlaufen, um sicherzustellen, dass alle Nebenwirkungen nur einmal auftreten. Zum Beispiel:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int GetAt(int index) => _array[index];
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get().GetAt(^1);
Console.WriteLine(i);
}
}
Dieser Code gibt "Get Length 3" aus.
Dieses Feature wäre für jedes Mitglied von Vorteil, das einen Parameter hat, der einen Index darstellt. Beispiel: List<T>.InsertAt
. Auch dies birgt Verwirrungspotenzial, da die Sprache keine Anleitung dazu geben kann, ob ein Ausdruck zur Indizierung gedacht ist oder nicht. Möglich ist lediglich, einen beliebigen Index
-Ausdruck in int
zu konvertieren, wenn ein Member in einem Countable-Typ aufgerufen wird.
Beschränkungen:
- Diese Konvertierung gilt nur, wenn der Ausdruck vom Typ
Index
direkt ein Argument für das Member ist. Sie gilt nicht für geschachtelte Ausdrücke.
Bei der Implementierung getroffene Entscheidungen
- Alle Member im Muster müssen Instanzmember sein.
- Wenn eine length-Methode gefunden wurde, die aber den falschen Rückgabetyp aufweist, sollten Sie weiterhin nach Count suchen.
- Der Indexer, der für das Indexmuster verwendet wird, muss genau einen Int-Parameter aufweisen.
- Die
Slice
-Methode, die für das Range-Muster verwendet wird, muss genau zwei Int-Parameter aufweisen. - Bei der Suche nach den strukturierten Mitgliedern suchen wir nach Originaldefinitionen, nicht nach konstruierten Mitgliedern
Designbesprechungen
C# feature specifications