Partage via


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. Le Index 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]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 de expr2 est int, il sera converti en receiver.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 :

  1. receiver est évalué ;
  2. expr est évalué ;
  3. length est évalué, si nécessaire ;
  4. 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 type int.
  • 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]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 forme expr1..expr2 (où expr2 peut être omis) et que expr1 est de type int, il est émis sous la forme expr1.
  • Lorsque expr est de la forme ^expr1..expr2 (où expr2 peut être omis), il est émis sous la forme receiver.Length - expr1.
  • Lorsque expr est de la forme ..expr2 (où expr2 peut être omis), il sera émis comme 0.
  • 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 forme expr1..expr2 (où expr1 peut être omis) et que expr2 est de type int, il est émis sous la forme expr2 - start.
  • Lorsque expr est de la forme expr1..^expr2 (où expr1 peut être omis), il est émis sous la forme (receiver.Length - expr2) - start.
  • Lorsque expr est de la forme expr1.. (où expr1 peut être omis), il sera émis comme receiver.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 :

  1. receiver est évalué ;
  2. expr est évalué ;
  3. length est évalué, si nécessaire ;
  4. 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éthode Substring sera utilisée au lieu de Slice.
  • array : la méthode System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray sera utilisée au lieu de Slice.

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 comme Span<T> sont idéaux pour la prise en charge des index/plages.
  • string : n'implémente pas ICollection 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 de ICollection et préfèrent donc Count plutôt que de se soucier de la longueur.
  • Use Count : exclut string, les tableaux, Span<T> et la plupart des types basés sur ref 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 de expr2 est int, il sera traduit en receiver.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