Plages
Remarque
Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Il inclut les modifications de spécification proposées, ainsi que les informations nécessaires pendant la conception et le développement de la fonctionnalité. Ces articles sont publiés jusqu’à ce que les modifications de spécification proposées soient finalisées et incorporées dans la spécification ECMA actuelle.
Il peut y avoir des différences entre la spécification de la fonctionnalité et l’implémentation terminée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).
Vous pouvez en savoir plus sur le processus d’adoption des speclets de fonctionnalités dans la norme de langage C# dans l’article sur les spécifications .
Résumé
Cette fonctionnalité consiste à fournir deux nouveaux opérateurs qui permettent de construire des objets System.Index
et System.Range
, et de les utiliser pour indexer/segmenter des collections au moment de l’exécution.
Aperçu
Types et membres connus
Pour utiliser les nouvelles formes syntactiques pour System.Index
et System.Range
, de nouveaux types et membres connus peuvent être nécessaires, selon les formes syntactiques utilisées.
Pour utiliser l’opérateur « chapeau » (^
), les éléments suivants sont requis
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
Pour utiliser le type System.Index
comme argument dans un accès à un élément de tableau, le membre suivant est requis :
int System.Index.GetOffset(int length);
La syntaxe ..
pour System.Range
nécessite le type System.Range
, ainsi qu’un ou plusieurs des membres suivants :
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; }
}
}
La syntaxe ..
permet que l'un, l'autre, les deux ou aucun des arguments ne soient absents. Quel que soit le nombre d’arguments, le constructeur Range
est toujours suffisant pour utiliser la syntaxe Range
. Toutefois, si l’un des autres membres est présent et qu’un ou plusieurs arguments ..
sont manquants, le membre approprié peut être remplacé.
Enfin, pour une valeur de type System.Range
à utiliser dans une expression d’accès aux éléments de tableau, le membre suivant doit être présent :
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}
System.Index
C# n’a aucun moyen d’indexer une collection à partir de la fin, mais la plupart des indexeurs utilisent la notion « from start » ou effectuent une expression « length - i ». Nous introduisons une nouvelle expression d'index qui signifie « à partir de la fin ». La fonctionnalité introduira un nouvel opérateur unaire préfixe « hat ». Son opérande unique doit être convertible en System.Int32
. Il sera abaissé dans l'appel de méthode de la fabrique System.Index
appropriée.
Nous augmentons la grammaire pour unary_expression avec la forme de syntaxe supplémentaire suivante :
unary_expression
: '^' unary_expression
;
Nous appelons cela l’index de l'opérateur terminal. Les opérateurs prédéfinis d'index à partir de la fin sont les suivants :
System.Index operator ^(int fromEnd);
Le comportement de cet opérateur est défini uniquement pour les valeurs d’entrée supérieures ou égales à zéro.
Exemples:
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# ne dispose d'aucun moyen syntaxique pour accéder à des « rangées » ou à des « subdivisions » des collections. En règle générale, les utilisateurs sont obligés d’implémenter des structures complexes pour filtrer/opérer sur des tranches de mémoire, ou recourir à des méthodes LINQ telles que list.Skip(5).Take(2)
. Avec l’ajout de System.Span<T>
et d’autres types similaires, il devient plus important d’avoir ce type d’opération pris en charge à un niveau plus approfondi dans le langage/runtime et que l’interface soit unifiée.
Le langage introduira un nouvel opérateur de plage x..y
. Il s'agit d'un opérateur binaire infixe qui accepte deux expressions. L'un ou l'autre des opérandes peut être omis (exemples ci-dessous), et ils doivent être convertibles en System.Index
. Il sera abaissé à l'appel de méthode de fabrique System.Range
approprié.
Nous remplaçons les règles de grammaire C# pour multiplicative_expression par les éléments suivants (afin d’introduire un nouveau niveau de priorité) :
range_expression
: unary_expression
| range_expression? '..' range_expression?
;
multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;
Toutes les formes de l'opérateur range ont la même priorité. Ce nouveau groupe de précédence est inférieur aux opérateurs unaires et supérieur aux opérateurs arithmétiques multipliés .
Nous appelons l'opérateur ..
l'opérateur d'intervalle. L’opérateur d'intervalle intégré peut être compris comme correspondant approximativement à l'invocation d'un opérateur intégré de cette forme :
System.Range operator ..(Index start = 0, Index end = ^0);
Exemples:
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]
En outre, System.Index
doit avoir une conversion implicite de System.Int32
, afin d’éviter la nécessité de surcharger le mélange d’entiers et d’index sur des signatures multidimensionnelles.
Ajout de la prise en charge des index et des plages aux types de bibliothèque existants
Prise en charge de l'index implicite
Le langage fournit un membre d’indexeur d’instance avec un paramètre unique de type Index
pour les types qui répondent aux critères suivants :
- Le type est dénombrable.
- Le type a un indexeur d’instance accessible qui prend une seule
int
en tant qu’argument. - Le type n’a pas d’indexeur d’instance accessible qui prend un
Index
comme premier paramètre. LeIndex
doit être le seul paramètre ou les paramètres restants doivent être facultatifs.
Un type est dénombrable s'il possède une propriété nommée Length
ou Count
avec un getter accessible et un type de retour int
. Le langage peut utiliser cette propriété pour convertir une expression de type Index
en un int
au point de l’expression sans avoir à utiliser le type Index
du tout. Si Length
et Count
sont tous deux présents, Length
sera préféré. Par souci de simplicité, la proposition utilisera le nom Length
pour représenter Count
ou Length
.
Pour ces types, la langue agit comme s’il existe un membre d’indexeur du formulaire T this[Index index]
où T
est le type de retour de l’indexeur basé sur int
, y compris les annotations de style ref
. Le nouveau membre aura les mêmes membres get
et set
avec une accessibilité correspondant à celle de l’indexeur int
.
Le nouvel indexeur est implémenté en convertissant l’argument de type Index
en int
et en émettant un appel à l’indexeur basé sur int
. À des fins de discussion, utilisons l’exemple de receiver[expr]
. La conversion de expr
en int
se produit comme suit :
- Lorsque l’argument est de la forme
^expr2
et que le type deexpr2
estint
, il est traduit enreceiver.Length - expr2
. - Sinon, elle sera traduite en tant que
expr.GetOffset(receiver.Length)
.
Quelle que soit la stratégie de conversion spécifique, l’ordre d’évaluation doit être équivalent à ce qui suit :
-
receiver
est évalué ; -
expr
est évalué ; -
length
est évalué, si nécessaire ; - l’indexeur basé sur
int
est appelé.
Cela permet aux développeurs d’utiliser la fonctionnalité de Index
sur les types existants sans avoir besoin de modification. Par exemple:
List<char> list = ...;
var value = list[^1];
// Gets translated to
var value = list[list.Count - 1];
Les expressions receiver
et Length
seront déversées de manière appropriée afin de garantir que tout effet secondaire ne soit exécuté qu'une seule fois. Par exemple:
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);
}
}
Ce code imprimera « Get Length 3 ».
Prise en charge de la plage implicite
Le langage fournit un membre d’indexeur d’instance avec un paramètre unique de type Range
pour les types qui répondent aux critères suivants :
- Le type est dénombrable.
- Le type a un membre accessible nommé
Slice
qui a deux paramètres de typeint
. - Le type n’a pas d’indexeur d’instance qui prend une seule
Range
comme premier paramètre. LeRange
doit être le seul paramètre ou les paramètres restants doivent être facultatifs.
Pour ces types, la liaison du langage se fera comme s’il existait un membre indexeur de la forme T this[Range range]
où T
est le type de retour de la méthode Slice
, en tenant compte de toute annotation de style ref
. Le nouveau membre aura également une accessibilité similaire à celle de Slice
.
Lorsque l'indexeur Range
, basé sur une expression nommée receiver
, est lié, il est abaissé en convertissant l'expression Range
en deux valeurs qui sont ensuite passées à la méthode Slice
. À des fins de discussion, utilisons l’exemple de receiver[expr]
.
Le premier argument de Slice
est obtenu en convertissant l’expression typée de plage de la manière suivante :
- Lorsque
expr
est de la formeexpr1..expr2
(oùexpr2
peut être omis) et queexpr1
a le typeint
, il est émis en tant queexpr1
. - Lorsque
expr
est de la forme^expr1..expr2
(oùexpr2
peut être omis), elle est émise en tant quereceiver.Length - expr1
. - Lorsque
expr
est de la forme..expr2
(oùexpr2
peut être omis), elle est émise en tant que0
. - Sinon, elle sera émise en tant que
expr.Start.GetOffset(receiver.Length)
.
Cette valeur sera réutilisée dans le calcul du deuxième argument Slice
. Dans ce cas, il sera appelé start
. Le deuxième argument de Slice
est obtenu en convertissant l’expression typée de plage de la manière suivante :
- Lorsque
expr
est de la formeexpr1..expr2
(oùexpr1
peut être omis) et queexpr2
a le typeint
, il est émis en tant queexpr2 - start
. - Lorsque
expr
est de la formeexpr1..^expr2
(oùexpr1
peut être omis), elle est émise en tant que(receiver.Length - expr2) - start
. - Lorsque
expr
est de la formeexpr1..
(oùexpr1
peut être omis), elle est émise en tant quereceiver.Length - start
. - Sinon, elle sera émise en tant que
expr.End.GetOffset(receiver.Length) - start
.
Quelle que soit la stratégie de conversion spécifique, l’ordre d’évaluation doit être équivalent à ce qui suit :
-
receiver
est évalué ; -
expr
est évalué ; -
length
est évalué, si nécessaire ; - la méthode
Slice
est appelée.
Les expressions receiver
, expr
et length
seront déversées comme il se doit pour garantir que les effets secondaires ne seront exécutés qu'une seule fois. Par exemple:
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);
}
}
Ce code imprimera « Get Length 2 ».
La langue traitera de manière spéciale les types connus suivants :
string
: la méthodeSubstring
sera utilisée au lieu deSlice
.array
: la méthodeSystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
sera utilisée au lieu deSlice
.
Alternatives
Les nouveaux opérateurs (^
et ..
) sont du sucre syntactique. La fonctionnalité peut être implémentée par des appels explicites aux méthodes de fabrique System.Index
et System.Range
, mais cela entraînera beaucoup plus de code standardisé, et l’expérience sera non-intuitive.
Représentation IL
Ces deux opérateurs seront réduits aux appels d’indexeur/méthode standard, sans modification des couches de compilateur suivantes.
Comportement du runtime
- Le compilateur peut optimiser les indexeurs pour les types intégrés tels que les tableaux et les chaînes, et réduire l’indexation aux méthodes existantes appropriées.
-
System.Index
sera rejeté s'il est construit avec une valeur négative. -
^0
n'est pas rejeté, mais il se traduit par la longueur de la collection ou de l'objet énumérable auquel il est fourni. Range.All
équivaut sémantiquement à0..^0
et peut être déconstructé à ces index.
Considérations
Détection d'un indexable basé sur une collection IC
Ce comportement a été inspiré par les initialisateurs de collections. En utilisant la structure d’un type pour transmettre qu’il avait choisi une fonctionnalité. Dans le cas des initialiseurs de collection, les types peuvent opter pour cette fonctionnalité en implémentant l'interface IEnumerable
(non générique).
Cette proposition exigeait initialement que les types implémentent ICollection
pour qu’ils se qualifient comme indexables. Cela nécessitait toutefois un certain nombre de cas spéciaux :
ref struct
: ces types ne peuvent pas encore implémenter d'interfaces, mais des types commeSpan<T>
sont idéaux pour la prise en charge des index/plages.string
: n’implémente pasICollection
et l’ajout de cette interface a un coût important.
Cela signifie que pour prendre en charge les types clés, un habillage spécial est déjà nécessaire. La casse spéciale de string
est moins intéressante, car le langage le fait dans d'autres domaines (abaissement, constantes, etc. foreach
...). Le casing spécial de ref struct
est plus préoccupant car il s'agit du casing spécial d'une classe entière de types. Ils sont étiquetés comme indexables s’ils ont simplement une propriété nommée Count
avec un type de retour de int
.
Après avoir examiné la conception a été normalisée pour dire que tout type qui a une propriété Count
/ Length
avec un type de retour de int
est indexable. Cela supprime tous les cas particuliers, même pour string
et les tableaux.
Détecter seulement le comptage
La détection sur les noms de propriétés Count
ou Length
complique un peu la conception. La sélection d’un seul pour normaliser n’est pas suffisante, car elle finit par exclure un grand nombre de types :
- Utilisez
Length
: exclut presque toutes les collections dans System.Collections et sous-espaces de noms. Ceux-ci ont tendance à dériver deICollection
et préfèrent doncCount
plutôt que de se soucier de la longueur. - Utilisez
Count
: exclutstring
, tableaux,Span<T>
et la plupart des types basés surref struct
La complication supplémentaire sur la détection initiale des types indexables est compensée par sa simplification dans d’autres aspects.
Choisir "Slice" comme nom
Le nom Slice
a été choisi comme nom standard de facto pour les opérations de type tranche dans .NET. A partir de netcoreapp2.1, tous les types de style étendue utilisent le nom Slice
pour les opérations de découpage. Avant netcoreapp2.1, il n'y a vraiment pas d'exemples de découpage disponibles. Les types comme List<T>
, ArraySegment<T>
, SortedList<T>
auraient été idéaux pour le découpage, mais le concept n’existait pas lorsque des types ont été ajoutés.
Ainsi, Slice
étant le seul exemple, il a été choisi comme nom.
Conversion du type cible de l'index
Une autre façon de considérer la transformation Index
dans une expression d’indexeur est de la voir comme une conversion vers un type cible. Au lieu de lier comme s'il existait un membre de la forme return_type this[Index]
, le langage attribue à int
une conversion de type cible.
Ce concept pourrait être généralisé à tous les accès aux membres des types dénombrables. Chaque fois qu’une expression de type Index
est utilisée comme argument pour un appel de membre d’instance et que le récepteur est Countable, l’expression aura une conversion de type cible en int
. Les appels membres applicables à cette conversion incluent des méthodes, des indexeurs, des propriétés, des méthodes d’extension, etc. Seuls les constructeurs sont exclus, car ils n’ont pas de récepteur.
La conversion de type cible est implémentée comme suit pour toute expression qui a un type de Index
. À des fins de discussion, utilisez l’exemple de receiver[expr]
:
- Lorsque
expr
est de la forme^expr2
et que le type deexpr2
estint
, il sera traduit enreceiver.Length - expr2
. - Sinon, elle sera traduite en tant que
expr.GetOffset(receiver.Length)
.
Les expressions receiver
et Length
seront déversées de manière appropriée afin de garantir que tout effet secondaire ne soit exécuté qu'une seule fois. Par exemple:
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);
}
}
Le code affichera « Longueur obtenue : 3 ».
Cette fonctionnalité serait bénéfique pour tout membre ayant un paramètre qui représentait un index. Par exemple, List<T>.InsertAt
. Cela a également le risque de confusion, car la langue ne peut pas donner de conseils quant à savoir si une expression est destinée ou non à l’indexation. Tout ce qu’il peut faire est de convertir n’importe quelle expression Index
en int
lors de l’appel d’un membre sur un type Countable.
Restrictions :
- Cette conversion s’applique uniquement lorsque l’expression avec le type
Index
est directement un argument au membre. Elle ne s’applique à aucune expression imbriquée.
Décisions prises lors de l’implémentation
- Tous les membres du modèle doivent être des membres d’instance
- Si une méthode Length est trouvée mais qu’elle a le type de retour incorrect, continuez à rechercher Count
- L’indexeur utilisé pour le modèle d’index doit avoir exactement un paramètre int
- La méthode
Slice
utilisée pour le modèle de plage doit avoir exactement deux paramètres int - Lorsque vous recherchez les membres du modèle, nous recherchons des définitions d’origine, et non des membres construits
Concevoir des réunions
C# feature specifications