Plages
Remarque
Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Elle inclut les changements de spécification proposés, ainsi que les informations nécessaires à la conception et au développement de la fonctionnalité. Ces articles sont publiés jusqu'à ce que les changements proposés soient finalisés et incorporés dans la spécification ECMA actuelle.
Il peut y avoir des divergences entre la spécification de la fonctionnalité et l'implémentation réalisée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).
Pour en savoir plus sur le processus d'adoption des speclets de fonctionnalité dans la norme du langage C#, consultez l'article sur les spécifications.
Problème de champion : https://github.com/dotnet/csharplang/issues/185
Récapitulatif
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/trancher des collections au moment de l'exécution.
Vue d'ensemble
Types et membres bien connus
Pour utiliser les nouvelles formes syntaxiques de System.Index
et System.Range
, de nouveaux types et membres bien connus peuvent être nécessaires, en fonction des formes syntaxiques 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 nécessaire :
int System.Index.GetOffset(int length);
La syntaxe ..
pour System.Range
nécessitera 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 des arguments de ..
sont manquants, le membre approprié peut être substitué.
Enfin, pour qu'une valeur de type System.Range
soit utilisée dans une expression d'accès à un élément 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 « à partir du début » ou une expression « longueur - 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 ajoutons à la grammaire de l'expression unary_expression la forme syntaxique 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 n'est défini que 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 général, les utilisateurs sont obligés de mettre en œuvre des structures complexes pour filtrer/opérer sur des tranches de mémoire, ou de recourir à des méthodes LINQ comme list.Skip(5).Take(2)
. Avec l'ajout de System.Span<T>
et d'autres types similaires, il devient plus important que ce type d'opération soit supporté à un niveau plus profond 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 ce qui suit (afin d'introduire un nouveau niveau de précédence) :
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éséance est inférieur aux opérateurs unaires et supérieur aux opérateurs arithmétiques multiplicatifs.
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]
De plus, System.Index
devrait 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 existants de la bibliothèque
Prise en charge de l'index implicite
Le langage fournira un membre indexeur d'instance avec un seul paramètre de type Index
pour les types qui répondent aux critères suivants :
- Le type est dénombrable.
- Le type possède un indexeur d'instance accessible qui prend un seul
int
comme 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 autres paramètres 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 qu'il soit nécessaire d'utiliser le type Index
. Si Length
et Count
sont tous deux présents, Length
sera préféré. Par souci de simplicité lors du transfert, la proposition utilisera le nom Length
pour représenter Count
ou Length
.
Pour ces types, le langage agira comme s'il existait un membre indexeur de la forme T this[Index index]
où T
est le type de retour de l'indexeur basé sur int
, y compris toutes 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 sera mis en œuvre en convertissant l'argument de type Index
en int
et en émettant un appel à l'indexeur basé sur int
. Pour les besoins de la discussion, prenons l'exemple de receiver[expr]
. La conversion de expr
en int
s'effectuera comme suit :
- Lorsque l'argument est de la forme
^expr2
et que le type deexpr2
estint
, il sera converti enreceiver.Length - expr2
. - Dans le cas contraire, il sera traduit par
expr.GetOffset(receiver.Length)
.
Indépendamment de 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 invoqué.
Cela permet aux développeurs d'utiliser la fonctionnalité Index
sur des types existants sans avoir à les modifier. 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. 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 fournira un indexeur d'instance avec un seul paramètre 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 un seul
Range
comme premier paramètre.Range
doit être le seul paramètre ou les autres paramètres 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
. Pour les besoins de la discussion, prenons l'exemple de receiver[expr]
.
Le premier argument de Slice
sera obtenu en convertissant l'expression typée en plage de la manière suivante :
- Lorsque
expr
est de la formeexpr1..expr2
(oùexpr2
peut être omis) et queexpr1
est de typeint
, il est émis sous la formeexpr1
. - Lorsque
expr
est de la forme^expr1..expr2
(oùexpr2
peut être omis), il est émis sous la formereceiver.Length - expr1
. - Lorsque
expr
est de la forme..expr2
(oùexpr2
peut être omis), il sera émis comme0
. - Dans le cas contraire, il sera émis sous la forme
expr.Start.GetOffset(receiver.Length)
.
Cette valeur sera réutilisée dans le calcul du deuxième argument Slice
. Ce faisant, elle sera désignée par start
. Le deuxième argument de Slice
est obtenu en convertissant l'expression typée en plage de la manière suivante :
- Lorsque
expr
est de la formeexpr1..expr2
(oùexpr1
peut être omis) et queexpr2
est de typeint
, il est émis sous la formeexpr2 - start
. - Lorsque
expr
est de la formeexpr1..^expr2
(oùexpr1
peut être omis), il est émis sous la forme(receiver.Length - expr2) - start
. - Lorsque
expr
est de la formeexpr1..
(oùexpr1
peut être omis), il sera émis commereceiver.Length - start
. - Dans le cas contraire, il sera émis sous la forme
expr.End.GetOffset(receiver.Length) - start
.
Indépendamment de 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 invoqué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. 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 syntaxique. 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 ramenés à des appels réguliers d'indexeur/méthode, sans changement dans les couches suivantes du compilateur.
Comportement en cours d'exécution
- Le compilateur peut optimiser les indexeurs pour les types intégrés tels que les tableaux et les chaînes, et abaisser 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
est sémantiquement équivalent à0..^0
, et peut être déconstruit jusqu'à ces indices.
Considérations
Détection d'un indexable basé sur une collection IC
Ce comportement a été inspiré par les initialisateurs de collections. Utilisation de la structure d'un type pour indiquer qu'il a opté pour 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
afin d'être qualifiés d'indexables. Cela nécessitait toutefois un certain nombre de cas particuliers :
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 int
.
Après réflexion, la conception a été normalisée pour dire que tout type ayant une propriété Count
/ Length
avec un type de retour 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. Le choix d'un seul type pour la normalisation n'est cependant pas suffisant, car il aboutit à l'exclusion d'un grand nombre de types :
- Use
Length
: exclut pratiquement toutes les collections de System.Collections et de leurs espaces de noms. Ceux-ci ont tendance à dériver deICollection
et préfèrent doncCount
plutôt que de se soucier de la longueur. - Use
Count
: exclutstring
, les tableaux,Span<T>
et la plupart des types basés surref struct
.
La complication supplémentaire lors de la détection initiale des types indexables est compensée par sa simplification à d'autres égards.
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. Des types tels que List<T>
, ArraySegment<T>
, SortedList<T>
auraient été idéaux pour le découpage en tranches, mais le concept n'existait pas lorsque les 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. Lorsqu'une expression de type Index
est utilisée comme argument d'une invocation d'un membre d'instance et que le récepteur est Countable, l'expression aura une conversion de type cible en int
. Les invocations de membres applicables à cette conversion comprennent les méthodes, les indexeurs, les propriétés, les méthodes d'extension, etc. Seuls les constructeurs sont exclus car ils n'ont pas de récepteur.
La conversion du type cible sera mise en œuvre comme suit pour toute expression dont le type est Index
. Pour les besoins de la discussion, prenons l'exemple de receiver[expr]
:
- Lorsque
expr
est de la forme^expr2
et que le type deexpr2
estint
, il sera traduit enreceiver.Length - expr2
. - Dans le cas contraire, il sera traduit par
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. 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);
}
}
Ce code imprimera « Get Length 3 ».
Cette fonctionnalité serait utile à tout membre dont le paramètre représente un index. Par exemple, List<T>.InsertAt
. Elle présente également un risque de confusion, car le langage ne peut pas indiquer si une expression est destinée à l'indexation ou non. Tout ce qu'il peut faire, c'est convertir toute expression Index
en int
lors de l'invocation d'un membre sur un type Countable.
Restrictions :
- Cette conversion ne s'applique que lorsque l'expression de type
Index
est directement un argument du membre. Elle ne s'applique pas aux expressions imbriquées.
Décisions prises lors de la mise en œuvre
- Tous les membres du motif doivent être des membres d'instance
- Si une méthode Length est trouvée mais que son type de retour n'est pas le bon, continuez à chercher Count.
- L'indexeur utilisé pour le motif Index doit avoir exactement un paramètre int.
- La méthode
Slice
utilisée pour le motif Range doit avoir exactement deux paramètres int. - Lors de la recherche des membres d'un motif, nous recherchons les définitions originales, et non les membres construits.
Réunions de conception
C# feature specifications