Partage via


Expressions de collection

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 .

Problème de champion : https://github.com/dotnet/csharplang/issues/8652

Résumé

Les expressions de collection introduisent une nouvelle syntaxe terse, [e1, e2, e3, etc], pour créer des valeurs de collection communes. L’incorporation d’autres collections dans ces valeurs est possible à l’aide d’un élément de propagation ..e comme suit : [e1, ..c2, e2, ..c2].

Plusieurs types de type collection peuvent être créés sans nécessiter de support BCL externe. Ces types sont les suivants :

Les types de type collection qui ne sont pas couverts par ce qui précède bénéficient d'une prise en charge supplémentaire grâce à un nouvel attribut et à un nouveau modèle d'API qui peuvent être adoptés directement sur le type lui-même.

Motivation

  • Les valeurs similaires à la collection sont très présentes dans la programmation, les algorithmes, et surtout dans l’écosystème C#/.NET. Presque tous les programmes utilisent ces valeurs pour stocker des données et envoyer ou recevoir des données d’autres composants. Actuellement, presque tous les programmes C# doivent utiliser de nombreuses approches différentes et malheureusement détaillées pour créer des instances de ces valeurs. Certaines approches présentent également des inconvénients de performances. Voici quelques exemples courants :

    • Les tableaux, qui nécessitent new Type[] ou new[] avant les valeurs { ... }.
    • Les étendues, qui peuvent utiliser stackalloc et d'autres constructions encombrantes.
    • Les initialisateurs de collections, qui requièrent une syntaxe comme new List<T> (sans inférence d'un T éventuellement détaillé) avant leurs valeurs, et qui peuvent provoquer de multiples réallocations de mémoire parce qu'ils utilisent N invocations .Add sans fournir de capacité initiale.
    • Collections immuables, qui nécessitent une syntaxe telle que ImmutableArray.Create(...) pour initialiser les valeurs, et qui peuvent entraîner des allocations intermédiaires et la copie de données. Les formes de construction plus efficaces (comme ImmutableArray.CreateBuilder) sont instables et produisent toujours des déchets inévitables.
  • En regardant l'écosystème environnant, nous trouvons également partout des exemples où la création de listes est plus pratique et agréable à utiliser. TypeScript, Dart, Swift, Elm, Python, et bien plus encore optez pour une syntaxe succincte à cet effet, avec une utilisation généralisée et à un grand effet. Les enquêtes superficielles n’ont révélé aucun problème de fond qui se pose dans ces écosystèmes avec ces littéraux intégrés.

  • C# a également ajouté des motifs de liste en C# 11. Ce modèle permet la correspondance et la déconstruction des valeurs de type liste à l’aide d’une syntaxe propre et intuitive. Toutefois, contrairement à presque toutes les autres constructions de modèle, cette syntaxe de correspondance/de déconstruction n’a pas la syntaxe de construction correspondante.

  • L’obtention des meilleures performances pour la construction de chaque type de collection peut être difficile. Les solutions simples gaspillent souvent à la fois le processeur et la mémoire. Le fait d’avoir un formulaire littéral permet une flexibilité maximale de l’implémentation du compilateur pour optimiser le littéral afin de produire au moins un résultat aussi bon qu’un utilisateur peut fournir, mais avec du code simple. Très souvent, le compilateur sera en mesure de faire mieux, et la spécification vise à permettre l’implémentation de grandes quantités de marge de manœuvre en termes de stratégie d’implémentation pour garantir cela.

Une solution inclusive est nécessaire pour C#. Cela devrait répondre à la grande majorité des cas pour les clients en termes de types et de valeurs de type collection qu'ils ont déjà. Il doit également sembler naturel dans la langue et refléter le travail effectué dans la reconnaissance de motifs.

Cela conduit à une conclusion naturelle que la syntaxe doit être semblable à [e1, e2, e3, e-etc] ou [e1, ..c2, e2], qui correspondent aux équivalents de modèle de [p1, p2, p3, p-etc] et de [p1, ..p2, p3].

Conception détaillée

Les productions grammaticales suivantes ont été ajoutées :

primary_no_array_creation_expression
  ...
+ | collection_expression
  ;

+ collection_expression
  : '[' ']'
  | '[' collection_element ( ',' collection_element )* ']'
  ;

+ collection_element
  : expression_element
  | spread_element
  ;

+ expression_element
  : expression
  ;

+ spread_element
  : '..' expression
  ;

Les littéraux de collection sont de type cible.

Clarifications des spécifications

  • Par souci de concision, collection_expression sera appelé « littéral » dans les sections suivantes.

  • expression_element instances sont généralement appelées e1, e_n, etc.

  • spread_element instances sont généralement appelées ..s1, ..s_n, etc.

  • Le type d'étendue signifie soit Span<T> ou ReadOnlySpan<T>.

  • Les littéraux seront généralement représentés par [e1, ..s1, e2, ..s2, etc] pour indiquer un nombre quelconque d'éléments dans n'importe quel ordre. Il est important de noter que ce formulaire sera utilisé pour représenter tous les cas tels que :

    • Littéraux vides []
    • Littéraux ne contenant pas de expression_element.
    • Littéraux ne contenant pas de spread_element.
    • Les littéraux avec un ordre arbitraire de n'importe quel type d'élément.
  • Le type d'itération de ..s_n est le type de la variable d'itération déterminé comme si s_n était utilisé comme expression itérée dans un foreach_statement.

  • Les variables commençant par __name sont utilisées pour représenter les résultats de l’évaluation de name, stockées dans un emplacement afin qu’elles ne soient évaluées qu’une seule fois. Par exemple, __e1 est l'évaluation de e1.

  • List<T>, IEnumerable<T>, etc. se réfèrent aux types respectifs dans l’espace de noms System.Collections.Generic.

  • La spécification définit une traduction du littéral vers des constructions C# existantes. Comme pour la traduction d’expressions de requête , le littéral est lui-même légal uniquement si la traduction entraînerait un code juridique. L’objectif de cette règle est d’éviter d’avoir à répéter d’autres règles de la langue implicite (par exemple, à propos de la convertibilité des expressions lorsqu’elles sont affectées aux emplacements de stockage).

  • Une implémentation n’est pas nécessaire pour traduire des littéraux exactement comme indiqué ci-dessous. Toute traduction est légale si le même résultat est produit et qu’il n’existe aucune différence observable dans la production du résultat.

    • Par exemple, une implémentation peut traduire des littéraux comme [1, 2, 3] directement vers une expression new int[] { 1, 2, 3 } qui lui-même fait cuire les données brutes dans l’assembly, éliminant ainsi la nécessité de __index ou d’une séquence d’instructions pour affecter chaque valeur. Important, cela signifie que si une étape de la traduction peut provoquer une exception lors de l’exécution que l’état du programme est toujours laissé dans l’état indiqué par la traduction.
  • Les références à l'« allocation sur la pile » désignent toute stratégie d'allocation sur la pile et non sur le tas. Il est important de noter que cela n'implique pas ou n'exige pas que cette stratégie passe par le mécanisme stackalloc proprement dit. Par exemple, l'utilisation de tableaux en ligne est également une approche autorisée et souhaitable pour réaliser l'allocation sur la pile lorsqu'elle est disponible. Notez que dans C# 12, les tableaux inline ne peuvent pas être initialisés avec une expression de collection. Cela reste une proposition ouverte.

  • Les collections sont supposées se comporter correctement. Par exemple:

    • Il est supposé que la valeur de Count sur une collection produira cette même valeur que le nombre d’éléments énumérés.
    • Les types utilisés dans cette spécification définie dans l’espace de noms System.Collections.Generic sont supposés être sans effet secondaire. Par conséquent, le compilateur peut optimiser les scénarios dans lesquels de tels types peuvent être utilisés comme valeurs intermédiaires, mais sinon ne pas être exposés.
    • On suppose qu'un appel à un membre .AddRange(x) applicable à une collection aboutira à la même valeur finale que si l'on itère sur x et que l'on ajoute toutes ses valeurs énumérées individuellement à la collection avec .Add.
    • Le comportement des littéraux de collection avec les collections qui ne sont pas bien gérées n'est pas défini.

Conversions

Une conversion d’expression de collection permet à une expression de collection d’être convertie en type.

Il existe une conversion implicite d'une expression de collection vers les types suivants :

  • un type de tableau unidimensionnelT[], auquel cas le type d'élément est T
  • Un type d'étendue :
    • System.Span<T>
    • System.ReadOnlySpan<T>
      dans les cas où le type d’élément est T
  • un type doté d'une méthode de création appropriée, auquel cas le type d'élément est le type d'itération déterminé à partir d'une méthode d'instance GetEnumerator ou d'une interface énumérable, et non à partir d'une méthode d'extension
  • Un type de structure ou de classe qui implémente System.Collections.IEnumerable où :
    • Le type a un constructeur applicable qui peut être invoqué sans arguments, et le constructeur est accessible à l'emplacement de l'expression de la collection.

    • Si l’expression de collection a des éléments, le type a une instance ou une méthode d’extension Add où :

      • La méthode peut être appelée avec un seul argument valeur.
      • Si la méthode est générique, les arguments de type peuvent être déduits de la collection et de l’argument.
      • La méthode est accessible à l'emplacement de l'expression de la collection.

      Dans ce cas, le type d’élément est le type d’itération du type .

  • Un type d'interface :
    • System.Collections.Generic.IEnumerable<T>
    • System.Collections.Generic.IReadOnlyCollection<T>
    • System.Collections.Generic.IReadOnlyList<T>
    • System.Collections.Generic.ICollection<T>
    • System.Collections.Generic.IList<T>
      dans les cas où le type d’élément est T

La conversion implicite existe si le type a un type d'élémentU où pour chaque élémentEᵢ de l'expression de collection :

  • Si Eᵢ est un élément d’expression , il existe une conversion implicite de Eᵢ en U.
  • Si Eᵢ est un élément de propagation ..Sᵢ, il existe une conversion implicite du type d’itération de Sᵢ en U.

Il n'y a pas de conversion d'une expression de collection vers un type de tableau multidimensionnel.

Les types pour lesquels il existe une conversion implicite d'expression de collection à partir d'une expression de collection sont les types cibles valides pour cette expression de collection.

Les conversions implicites supplémentaires suivantes existent à partir d’une expression de collection :

  • Vers un type de valeur nullableT? lorsqu'il existe une conversion d'expression de collection de l'expression de collection vers un type de valeur T. La conversion est une conversion d'expression de collection vers T suivie d'une conversion nullable implicite de T vers T?.

  • Vers un type de référence T où il existe une méthode de création associée à T qui renvoie un type U et une conversion implicite de référence de U à T. La conversion comprend d'abord une conversion d'expression de collection en U, suivie d'une conversion implicite de référence de U en T.

  • Vers un type d'interface I où il existe une méthode de création associée à I qui renvoie un type V et une conversion implicite de boxing de V en I. La conversion est une conversion d'expression de collection en V suivie d'une conversion de boîte implicite de V en I.

Créer des méthodes

Une méthode de création est indiquée par un attribut [CollectionBuilder(...)] sur le type de collection. L’attribut spécifie le type de générateur et nom de méthode d’une méthode à appeler pour construire une instance du type de collection.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(
        AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface,
        Inherited = false,
        AllowMultiple = false)]
    public sealed class CollectionBuilderAttribute : System.Attribute
    {
        public CollectionBuilderAttribute(Type builderType, string methodName);
        public Type BuilderType { get; }
        public string MethodName { get; }
    }
}

L’attribut peut être appliqué à un class, struct, ref structou interface. L’attribut n’est pas hérité bien que l’attribut puisse être appliqué à une class de base ou à un abstract class.

Le type de constructeur doit être un class ou un struct non générique.

Tout d’abord, l’ensemble de méthodes de création applicablesCM est déterminé.
Il se compose de méthodes qui répondent aux exigences suivantes :

  • La méthode doit avoir le nom spécifié dans l’attribut [CollectionBuilder(...)].
  • La méthode doit être définie directement sur le type constructeur .
  • La méthode doit être static.
  • La méthode doit être accessible à l’endroit où l’expression de collection est utilisée.
  • L'arité de la méthode doit correspondre à l'arité du type de collection.
  • La méthode doit avoir un paramètre unique de type System.ReadOnlySpan<E>, passé par valeur.
  • Il existe une conversion d'identité, une conversion de référence implicite ou une conversion de mise en boîte entre le type de retour de la méthode et le type de collection.

Les méthodes déclarées sur les types de base ou les interfaces sont ignorées et ne font pas partie de l’ensemble de CM.

Si l'ensemble CM est vide, alors le type de collection ne contient pas de type d’élément et il ne possède pas non plus de méthode de création . Aucune des étapes suivantes ne s’applique.

Si une seule méthode de l'ensemble CM présente une conversion d'identité de E vers le type d'élément du type de collection, il s'agit de la méthode de création du type de collection. Sinon, le type de collection n’a pas de méthode de création.

Une erreur est signalée si l’attribut [CollectionBuilder] ne fait pas référence à une méthode appelante avec la signature attendue.

Pour une expression de collection avec un type cible C<S0, S1, …> où la déclaration de typeC<T0, T1, …> a une méthode de construction associéeB.M<U0, U1, …>(), les arguments de type générique du type cible sont appliqués dans l'ordre - et du type contenant le plus externe au plus interne - à la méthode de construction.

Le paramètre de l'étendue pour la méthode de création peut être explicitement marqué scoped ou [UnscopedRef]. Si le paramètre est implicitement ou explicitement scoped, le compilateur peut allouer le stockage pour l'étendue sur la pile plutôt que sur le tas.

Par exemple, une méthode de création possible pour ImmutableArray<T> :

[CollectionBuilder(typeof(ImmutableArray), "Create")]
public struct ImmutableArray<T> { ... }

public static class ImmutableArray
{
    public static ImmutableArray<T> Create<T>(ReadOnlySpan<T> items) { ... }
}

Avec la méthode create ci-dessus, ImmutableArray<int> ia = [1, 2, 3]; pourrait être émis comme suit :

[InlineArray(3)] struct __InlineArray3<T> { private T _element0; }

Span<int> __tmp = new __InlineArray3<int>();
__tmp[0] = 1;
__tmp[1] = 2;
__tmp[2] = 3;
ImmutableArray<int> ia =
    ImmutableArray.Create((ReadOnlySpan<int>)__tmp);

Construction

Les éléments d'une expression de collection sont évalués dans l'ordre, de gauche à droite. Chaque élément est évalué exactement une fois, et toutes les autres références aux éléments font référence aux résultats de cette évaluation initiale.

Un élément d'étendue peut être itéré avant ou après l'évaluation des éléments suivants de l'expression de collection.

Une exception non gérée lancée par l'une des méthodes utilisées au cours de la construction ne sera pas capturée et empêchera les étapes suivantes de la construction.

Length, Countet GetEnumerator sont supposés n’avoir aucun effet secondaire.


Si le type cible est un struct ou un type de classe qui implémente System.Collections.IEnumerable, et que le type cible n’a pas de méthode create , la construction de l’instance de collection est la suivante :

  • Les éléments sont évalués dans l’ordre. Certains éléments ou tous les éléments peuvent être évalués au cours des étapes suivantes plutôt qu'avant.

  • Le compilateur peut déterminer la longueur connue de l'expression de la collection en invoquant des propriétés dénombrables - ou des propriétés équivalentes d'interfaces ou de types bien connus - sur chaque expression d'élément étalé.

  • Le constructeur applicable sans arguments est invoqué.

  • Pour chaque élément dans l’ordre :

    • Si l’élément est un élément d’expression , la méthode d’instance ou d’extension Add applicable est appelée avec l’élément expression comme argument. (Contrairement au comportement classique d'un initialisateur de collection, l'évaluation des éléments et les appels à Add ne sont pas nécessairement imbriqués).
    • Si l’élément est un élément de propagation l’un des éléments suivants est utilisé :
      • Une méthode d’instance ou d’extension applicable GetEnumerator est appelée sur l'expression d'élément de propagation et pour chaque élément de l’énumérateur, la méthode d’instance ou d’extension applicable Add est appelée sur l’instance de collection avec l'élément comme argument. Si l’énumérateur implémente IDisposable, Dispose sera appelé après l’énumération, quelles que soient les exceptions.
      • Une instance AddRange ou une méthode d'extension applicable est invoquée sur l'instance de collection avec l'expression de l'élément étalé comme argument.
      • Une instance ou une méthode d'extension CopyTo applicable est invoquée sur l'expression de l'élément étalé avec l'instance de la collection et l'index int comme arguments.
  • Au cours des étapes de construction ci-dessus, une instance EnsureCapacity ou une méthode d'extension applicable peut être invoquée une ou plusieurs fois sur l'instance de collection avec une capacité int comme argument.


Si le type cible est un tableau , une étendue , un type avec une méthode de création , ou une interface , la construction de l’instance de collection se fait comme suit :

  • Les éléments sont évalués dans l’ordre. Certains éléments ou tous les éléments peuvent être évalués au cours des étapes suivantes plutôt qu'avant.

  • Le compilateur peut déterminer la longueur connue de l'expression de la collection en invoquant des propriétés dénombrables - ou des propriétés équivalentes d'interfaces ou de types bien connus - sur chaque expression d'élément étalé.

  • Une instance d’initialisation est créée comme suit :

    • Si le type cible est un tableau et que l’expression de collection a une longueur connue, un tableau est alloué avec la longueur attendue.
    • Si le type cible est une étendue ou un type avec une méthode de création , et que la collection a une longueur connue, une étendue avec la longueur attendue est créée en faisant référence au stockage contigu.
    • Dans le cas contraire, un stockage intermédiaire est alloué.
  • Pour chaque élément dans l’ordre :

    • Si l’élément est un élément d’expression , l’instance d’initialisation indexeur est appelée pour ajouter l’expression évaluée à l’index actuel.
    • Si l’élément est un élément de propagation l’un des éléments suivants est utilisé :
      • Un membre d'une interface ou d'un type bien connu est invoqué pour copier les éléments de l'expression de l'élément étalé vers l'instance d'initialisation.
      • Une méthode d'instance ou d'extension GetEnumerator applicable est invoquée sur l'expression de l'élément d'étalement et, pour chaque élément de l'énumérateur, l'indexeur de l'instance d'initialisation est invoqué pour ajouter l'élément à l'index courant. Si l’énumérateur implémente IDisposable, Dispose sera appelé après l’énumération, quelles que soient les exceptions.
      • Une instance CopyTo ou une méthode d'extension applicable est invoquée sur l'expression de l'élément d'étalement avec l'instance d'initialisation et l'index int comme arguments.
  • Si le stockage intermédiaire a été alloué pour la collection, une instance de collection est allouée avec la longueur de collection réelle et les valeurs de l’instance d’initialisation sont copiées dans l’instance de collection, ou si une étendue est requise, le compilateur peut utiliser une étendue de la longueur réelle de la collection à partir du stockage intermédiaire. Sinon, l’instance d’initialisation est l’instance de collection.

  • Si le type cible possède une méthode de création, la méthode de création est invoquée avec l'instance d'étendue.


Remarque : Le compilateur peut retarder l’ajout d’éléments à la collection, ou retarder l’itération des éléments répartis jusqu'à ce que les éléments suivants aient été évalués. (Lorsque les éléments étendus suivants ont des propriétés dénombrables qui permettraient de calculer la longueur attendue de la collection avant d'allouer la collection). Inversement, le compilateur peut ajouter rapidement des éléments à la collection - et itérer rapidement à travers les éléments étalés - lorsqu'il n'y a pas d'avantage à attendre.

Considérez l'expression de collection suivante :

int[] x = [a, ..b, ..c, d];

Si les éléments étalés b et c sont dénombrables, le compilateur peut retarder l'ajout d'éléments de a et b jusqu'à ce que c soit évalué, afin d'allouer le tableau résultant à la longueur attendue. Après cela, le compilateur pourrait ajouter avec impatience des éléments de c, avant d’évaluer d.

var __tmp1 = a;
var __tmp2 = b;
var __tmp3 = c;
var __result = new int[2 + __tmp2.Length + __tmp3.Length];
int __index = 0;
__result[__index++] = __tmp1;
foreach (var __i in __tmp2) __result[__index++] = __i;
foreach (var __i in __tmp3) __result[__index++] = __i;
__result[__index++] = d;
x = __result;

Littéral de collection vide

  • Le littéral vide [] n’a aucun type. Cependant, à l'instar du littéral nul, ce littéral peut être implicitement converti en n'importe quel type de collection constructible.

    Par exemple, le code suivant n’est pas légal, car il n’existe aucun type cible et qu'il n'y a pas d'autres conversions impliquées :

    var v = []; // illegal
    
  • L'étalement d'un littéral vide peut être élidé. Par exemple:

    bool b = ...
    List<int> l = [x, y, .. b ? [1, 2, 3] : []];
    

    Ici, si b est false, il n’est pas nécessaire qu’aucune valeur soit réellement construite pour l’expression de collection vide, car elle serait immédiatement répartie en valeurs zéro dans le littéral final.

  • L’expression de collection vide est autorisée à être un singleton si elle est utilisée pour construire une valeur de collection finale connue pour ne pas être mutable. Par exemple:

    // Can be a singleton, like Array.Empty<int>()
    int[] x = []; 
    
    // Can be a singleton. Allowed to use Array.Empty<int>(), Enumerable.Empty<int>(),
    // or any other implementation that can not be mutated.
    IEnumerable<int> y = [];
    
    // Must not be a singleton.  Value must be allowed to mutate, and should not mutate
    // other references elsewhere.
    List<int> z = [];
    

Sécurité des références

Voir la contrainte de contexte sûr pour les définitions des valeurs de safe-context : declaration-block, function-member et caller-context.

Le safe-context d'une expression de collection est :

  • Le safe-context d'une expression de collection vide [] est le caller-context.

  • Si le type cible est un type d'étendueSystem.ReadOnlySpan<T> et que T est l'un des types primitifbool sbyte, byte, short, ushort, char, int, uint, long, ulong, float, ou double, et que l'expression de collection ne contient que des valeurs constantes, le safe-context de l'expression de collection est le caller-context.

  • Si le type cible est un type d'étendueSystem.Span<T> ou System.ReadOnlySpan<T>, le safe-context de l'expression de collection est le declaration-block.

  • Si le type cible est un type ref struct avec une méthode de création, le contexte sûr de l'expression de collection est le contexte sûr d'une invocation de la méthode de création où l'expression de collection est l'argument étendue de la méthode.

  • Sinon, le safe-context de l'expression de collection est le caller-context.

Une expression de collection avec un safe-context de déclaration-block ne peut pas s'échapper de l'étendue qui l'entoure, et le compilateur peut stocker la collection sur la pile plutôt que sur le tas.

Pour permettre à une expression de collection pour un type struct ref d’échapper au bloc de déclaration , il est parfois nécessaire de convertir l’expression en un autre type.

static ReadOnlySpan<int> AsSpanConstants()
{
    return [1, 2, 3]; // ok: span refers to assembly data section
}

static ReadOnlySpan<T> AsSpan2<T>(T x, T y)
{
    return [x, y];    // error: span may refer to stack data
}

static ReadOnlySpan<T> AsSpan3<T>(T x, T y, T z)
{
    return (T[])[x, y, z]; // ok: span refers to T[] on heap
}

Inférence de type

var a = AsArray([1, 2, 3]);          // AsArray<int>(int[])
var b = AsListOfArray([[4, 5], []]); // AsListOfArray<int>(List<int[]>)

static T[] AsArray<T>(T[] arg) => arg;
static List<T[]> AsListOfArray<T>(List<T[]> arg) => arg;

Les règles d’inférence de type sont mises à jour comme suit.

Les règles existantes pour la première phase sont extraites vers une nouvelle section d'inférence de type d'entrée, et une règle est ajoutée à l'inférence de type d'entrée et à l'inférence de type de sortie pour les expressions de collection.

11.6.3.2 La première phase

Pour chacun des arguments de méthode Eᵢ:

  • Une inférence de type d'entrée est effectuée à partirEᵢ du type de paramètre correspondantTᵢ.

Une inférence de type d'entrée est effectuée à partir d'une expression Evers un type T de la manière suivante :

  • Si E est une expression de collection avec des éléments Eᵢ, et T est un type avec un type d'élémentTₑ ou T est un type de valeur nullableT0? et T0 a un type d'élémentTₑ, alors pour chaque Eᵢ :
    • Si Eᵢ est un élément d'expression, une inférence de type d'entrée est faite deEᵢ Tₑà.
    • Si Eᵢ est un élément d'étalement avec un type d'itérationSᵢ, alors une inférence de borne inférieure est faite à partir Sᵢde Tₑà.
  • [règles existantes de la première phase] ...

11.6.3.7 Inférences de type de sortie

Une inférence de type de sortie est faite à partir d'une expressionE vers un type T de la manière suivante :

  • Si E est une expression de collection avec des éléments Eᵢ, et T est un type avec un type d'élémentTₑ ou T est un type de valeur nullableT0? et T0 a un type d'élémentTₑ, alors pour chaque Eᵢ :
    • Si Eᵢ est un élément d'expression, une inférence de type de sortie est faite deEᵢ à.Tₑ
    • Si Eᵢ est un élément de propagation , aucune inférence n’est effectuée à partir de Eᵢ.
  • [règles existantes des inférences de type de sortie] ...

Méthodes d’extension

Aucune modification des règles d'invocation des méthodes d'extension.

12.8.10.3 Appels de méthode d’extension

Une méthode d’extension Cᵢ.Mₑ est admissible si :

  • ...
  • Une conversion implicite d'identité, de référence ou de boîte existe de expr vers le type du premier paramètre de Mₑ.

Une expression de collection n’a pas de type naturel, de sorte que les conversions existantes de type ne sont pas applicables. Par conséquent, une expression de collection ne peut pas être utilisée directement comme premier paramètre pour un appel de méthode d’extension.

static class Extensions
{
    public static ImmutableArray<T> AsImmutableArray<T>(this ImmutableArray<T> arg) => arg;
}

var x = [1].AsImmutableArray();           // error: collection expression has no target type
var y = [2].AsImmutableArray<int>();      // error: ...
var z = Extensions.AsImmutableArray([3]); // ok

Résolution de surcharge

Une meilleure conversion à partir d'une expression est mise à jour pour préférer certains types cibles dans les conversions d'expressions de collection.

Dans les règles mises à jour :

  • Une span_type est l’une des suivantes :
    • System.Span<T>
    • System.ReadOnlySpan<T>.
  • Une array_or_array_interface est l'une des suivantes :
    • un type de tableau
    • l'un des types d'interface suivants implémentés par un type de tableau :
      • System.Collections.Generic.IEnumerable<T>
      • System.Collections.Generic.IReadOnlyCollection<T>
      • System.Collections.Generic.IReadOnlyList<T>
      • System.Collections.Generic.ICollection<T>
      • System.Collections.Generic.IList<T>

Étant donné une conversion implicite C₁ qui convertit d’une expression E en type T₁, et une conversion implicite C₂ qui convertit d’une expression E en type T₂, C₁ est une meilleure conversion que C₂ si l’une des opérations suivantes contient :

  • Eest une expression de collection et l'une des conditions suivantes est remplie :
    • T₁ est System.ReadOnlySpan<E₁>et T₂ est System.Span<E₂>et une conversion implicite existe de E₁ à E₂
    • T₁ est System.ReadOnlySpan<E₁> ou System.Span<E₁>, et T₂ est une array_or_array_interface ou tableau avec un type d'élémentE₂, et une conversion implicite existe de E₁ à E₂
    • T₁ n’est pas un span_typeet T₂ n’est pas un span_typeet une conversion implicite existe de T₁ à T₂
  • E n'est pas une expression de collection et l'une des conditions suivantes est remplie :
    • E correspond exactement à T₁ et E ne correspond pas exactement aux T₂
    • E correspond exactement soit à la fois à T₁ et à T₂, soit ni à l'un ni à l'autre, et T₁ est une meilleure cible de conversion que T₂.
  • E est un groupe de méthodes, ...

Exemples de différences dans la résolution de surcharge entre les initialiseurs de tableau et les expressions de collection :

static void Generic<T>(Span<T> value) { }
static void Generic<T>(T[] value) { }

static void SpanDerived(Span<string> value) { }
static void SpanDerived(object[] value) { }

static void ArrayDerived(Span<object> value) { }
static void ArrayDerived(string[] value) { }

// Array initializers
Generic(new[] { "" });      // string[]
SpanDerived(new[] { "" });  // ambiguous
ArrayDerived(new[] { "" }); // string[]

// Collection expressions
Generic([""]);              // Span<string>
SpanDerived([""]);          // Span<string>
ArrayDerived([""]);         // ambiguous

Types d'étendues

Les types d'étendues ReadOnlySpan<T> et Span<T> sont tous deux des types de collection constructibles. Leur prise en charge suit la conception de params Span<T>. Plus précisément, la construction de l'une de ces portées entraîne la création d'un tableau T[] sur la pile si le tableau de paramètres respecte les limites éventuellement fixées par le compilateur. Sinon, le tableau sera alloué sur le tas.

Si le compilateur choisit d'allouer sur la pile, il n'est pas tenu de traduire un littéral directement en stackalloc à ce moment précis. Par exemple, étant donné :

foreach (var x in y)
{
    Span<int> span = [a, b, c];
    // do things with span
}

Le compilateur est autorisé à traduire cela à l’aide de stackalloc tant que la signification de Span reste la même et que la sécurité d’étendue de est maintenue. Par exemple, il peut traduire ce qui précède en :

Span<int> __buffer = stackalloc int[3];
foreach (var x in y)
{
    __buffer[0] = a
    __buffer[1] = b
    __buffer[2] = c;
    Span<int> span = __buffer;
    // do things with span
}

Le compilateur peut également utiliser des tableaux en ligne, s'ils sont disponibles, lorsqu'il choisit d'allouer sur la pile. Notez que dans C# 12, les tableaux inline ne peuvent pas être initialisés avec une expression de collection. Cette fonctionnalité est une proposition ouverte.

Si le compilateur décide d'allouer sur le tas, la traduction de Span<T> est simple :

T[] __array = [...]; // using existing rules
Span<T> __result = __array;

Traduction littérale de la collection

Une expression de collection a une longueur connue si le type compile-time de chaque élément répandu dans l'expression de collection est dénombrable.

Traduction d’interface

Traduction d'une interface non modifiable

Étant donné un type cible qui ne contient pas de membres mutants, à savoir IEnumerable<T>, IReadOnlyCollection<T>et IReadOnlyList<T>, une implémentation conforme est nécessaire pour produire une valeur qui implémente cette interface. Si un type est synthétisé, il est recommandé que le type synthétisé implémente toutes ces interfaces, ainsi que ICollection<T> et IList<T>, quel que soit le type d’interface ciblé. Cela garantit une compatibilité maximale avec les bibliothèques existantes, y compris celles qui introspectent les interfaces mises en œuvre par une valeur afin d'optimiser les performances.

En outre, la valeur doit implémenter les interfaces ICollection et IList non génériques. Cela permet aux expressions de collection de prendre en charge l’introspection dynamique dans des scénarios tels que la liaison de données.

Une implémentation conforme est gratuite pour :

  1. Utilisez un type existant qui implémente les interfaces requises.
  2. Synthétisez un type qui implémente les interfaces requises.

Dans les deux cas, le type utilisé est autorisé à implémenter un plus grand ensemble d’interfaces que ceux strictement requis.

Les types synthétisés sont libres d’utiliser n’importe quelle stratégie qu’ils souhaitent implémenter correctement les interfaces requises. Par exemple, un type synthétisé peut intégrer les éléments directement en son sein, évitant ainsi la nécessité d'allocations de collections internes supplémentaires. Un type synthétisé n’a pas pu également utiliser de stockage quelconque, en choisissant de calculer les valeurs directement. Par exemple, en renvoyant index + 1 pour [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].

  1. La valeur doit retourner true lorsqu’elle est interrogée pour ICollection<T>.IsReadOnly (en cas d’implémentation) ainsi que pour IList.IsReadOnly non générique et IList.IsFixedSize. Cela permet aux consommateurs de savoir que la collection n'est pas mutable, même si les vues mutables sont implémentées.
  2. La valeur doit être renvoyée lors de tout appel à une méthode de mutation (comme IList<T>.Add). Cela garantit la sécurité, ce qui empêche une collection non mutable d’être mutée accidentellement.

Traduction de l'interface mutable

Type cible donné qui contient des membres mutants, à savoir ICollection<T> ou IList<T>:

  1. La valeur doit être une instance de List<T>.

Traduction de la longueur connue

Le fait d’avoir une longueur connue permet une construction efficace d’un résultat sans possibilité de copie de données et d’espace de marge inutile dans un résultat.

L'absence de longueur connue n'empêche pas la création d'un résultat. Toutefois, cela peut entraîner des coûts supplémentaires de processeur et de mémoire produisant les données, puis passer à la destination finale.

  • Pour un littéral de longueur connue [e1, ..s1, etc], la traduction commence d'abord par ce qui suit :

    int __len = count_of_expression_elements +
                __s1.Count;
                ...
                __s_n.Count;
    
  • Étant donné un type cible T pour ce littéral :

    • Si T est un T1[] quelconque, le littéral est traduit comme suit :

      T1[] __result = new T1[__len];
      int __index = 0;
      
      __result[__index++] = __e1;
      foreach (T1 __t in __s1)
          __result[__index++] = __t;
      
      // further assignments of the remaining elements
      

      L’implémentation est autorisée à utiliser d’autres moyens pour remplir le tableau. Par exemple, en utilisant des méthodes de copie en bloc efficaces comme .CopyTo().

    • Si T est un Span<T1>, le littéral est traduit comme ci-dessus, sauf que l’initialisation __result est traduite comme suit :

      Span<T1> __result = new T1[__len];
      
      // same assignments as the array translation
      

      La traduction peut utiliser stackalloc T1[] ou un tableau inline plutôt que new T1[] si la sécurité de portée est maintenue.

    • Si T est un ReadOnlySpan<T1>, le littéral est traduit de la même façon que pour le cas Span<T1>, sauf que le résultat final sera que Span<T1>converti implicitement en un ReadOnlySpan<T1>.

      Un ReadOnlySpan<T1>T1 est un type primitif et où tous les éléments de la collection sont constants n'a pas besoin que ses données soient sur le tas ou sur la pile. Par exemple, une implémentation peut construire cette étendue directement en tant que référence à une partie du segment de données du programme.

      Les formes ci-dessus (pour les tableaux et les étendues) sont les représentations de base de l'expression de collection et sont utilisées pour les règles de traduction suivantes :

      • Si T est un C<S0, S1, …> qui a une méthode de création correspondanteB.M<U0, U1, …>(), le littéral se traduit par :

        // Collection literal is passed as is as the single B.M<...>(...) argument
        C<S0, S1, …> __result = B.M<S0, S1, …>([...])
        

        Comme la méthode create doit avoir un type d'argument d'un certain ReadOnlySpan<T> instancié, la règle de traduction pour les étendues s'applique lors de la transmission de l'expression de collection à la méthode create.

      • Si T prend en charge les initialisateurs de collection, alors :

        • si le type T contient un constructeur accessible avec un paramètre unique int capacity, le littéral est traduit comme suit :

          T __result = new T(capacity: __len);
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          Remarque : le nom du paramètre doit être capacity.

          Cette forme permet à un littéral d'informer le type nouvellement construit du nombre d'éléments afin de permettre une allocation efficace du stockage interne. Cela évite les réaffectations inutiles à mesure que les éléments sont ajoutés.

        • sinon, le terme littéral est traduit de la manière suivante :

          T __result = new T();
          
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          Cela permet de créer le type cible, même si aucune optimisation de la capacité n’empêche la réaffectation interne du stockage.

Traduction de longueur inconnue

  • Étant donné un type cible T pour un littéral de longueur inconnue :

    • Si T prend en charge les initialisateurs de collection, le littéral est traduit par :

      T __result = new T();
      
      __result.Add(__e1);
      foreach (var __t in __s1)
          __result.Add(__t);
      
      // further additions of the remaining elements
      

      Cela permet la propagation de n’importe quel type itérable, bien qu’avec la moindre quantité d’optimisation possible.

    • Si T est un T1[], le littéral a la même sémantique que :

      List<T1> __list = [...]; /* initialized using predefined rules */
      T1[] __result = __list.ToArray();
      

      Les versions ci-dessus sont inefficaces ; il crée la liste intermédiaire, puis crée une copie du tableau final à partir de celui-ci. Les implémentations sont gratuites pour optimiser cela, par exemple produire du code comme suit :

      T1[] __result = <private_details>.CreateArray<T1>(
          count_of_expression_elements);
      int __index = 0;
      
      <private_details>.Add(ref __result, __index++, __e1);
      foreach (var __t in __s1)
          <private_details>.Add(ref __result, __index++, __t);
      
      // further additions of the remaining elements
      
      <private_details>.Resize(ref __result, __index);
      

      Cela permet un gaspillage minimal et une copie, sans surcharge supplémentaire que les collections de bibliothèques peuvent entraîner.

      Les décomptes transmis à CreateArray sont utilisés pour fournir un indice de taille initiale afin d'éviter les redimensionnements inutiles.

    • Si T est un type d'étendue, une implémentation peut suivre la stratégie T[] ci-dessus, ou toute autre stratégie ayant la même sémantique, mais de meilleures performances. Par exemple, au lieu d’allouer le tableau en tant que copie des éléments de liste, CollectionsMarshal.AsSpan(__list) peut être utilisé pour obtenir directement une valeur d’étendue.

Scénarios non pris en charge

Si les littéraux de collection peuvent être utilisés dans de nombreux cas, ils ne sont pas en mesure d'en remplacer certains. Voici quelques-uns des éléments suivants :

  • Tableaux multidimensionnels (par exemple, new int[5, 10] { ... }). Il n'est pas possible d'inclure les dimensions, et tous les littéraux de collection sont soit linéaires, soit des structures de mappage uniquement.
  • Collections qui transmettent des valeurs spéciales à leurs constructeurs. Il n’existe aucune facilité d’accès au constructeur utilisé.
  • Initialisateurs de collection imbriqués, par exemple new Widget { Children = { w1, w2, w3 } }. Ce formulaire doit rester car il a une sémantique très différente de Children = [w1, w2, w3]. Le premier appelle .Add de manière répétée sur .Children tandis que le second attribue une nouvelle collection à .Children. Nous pourrions envisager que cette dernière forme revienne à l'ajout à une collection existante si .Children ne peut pas être assigné, mais cela semble extrêmement confus.

Ambiguïtés de syntaxe

  • Il existe deux ambiguïtés syntactiques « vraies » où il existe plusieurs interprétations syntactiques juridiques du code qui utilisent un collection_literal_expression.

    • Le spread_element est ambigu avec un range_expression. On pourrait techniquement avoir :

      Range[] ranges = [range1, ..e, range2];
      

      Pour résoudre ce problème, nous pouvons :

      • Demander aux utilisateurs de mettre entre parenthèses (..e) ou d’inclure un index de démarrage 0..e s’ils veulent une plage.
      • Choisissez une syntaxe différente (comme ...) pour la propagation. Cela serait regrettable en raison de l'absence de cohérence avec les schémas de coupe.
  • Il existe deux cas où il n’y a pas d’ambiguïté réelle, mais où la syntaxe augmente considérablement la complexité de l’analyse. Bien qu’il ne s’agit pas d’un problème donné au moment de l’ingénierie, cela augmente encore la surcharge cognitive pour les utilisateurs lors de l’analyse du code.

    • Ambiguïté entre collection_literal_expression et attributes sur les instructions ou les fonctions locales. Considérer:

      [X(), Y, Z()]
      

      Il peut s’agir de l’un des suivants :

      // A list literal inside some expression statement
      [X(), Y, Z()].ForEach(() => ...);
      
      // The attributes for a statement or local function
      [X(), Y, Z()] void LocalFunc() { }
      

      Sans lookahead complexe, il serait impossible de le savoir sans consommer l'intégralité du littéral.

      Les options à prendre en compte sont les suivantes :

      • Autorisez cela, en effectuant le travail d'analyse pour déterminer de quel cas il s'agit.
      • Ne l'autorisez pas et exigez de l'utilisateur qu'il mette le littéral entre parenthèses comme ([X(), Y, Z()]).ForEach(...).
      • Ambiguïté entre un collection_literal_expression dans un conditional_expression et un null_conditional_operations. Considérer:
      M(x ? [a, b, c]
      

      Il peut s’agir de l’un des suivants :

      // A ternary conditional picking between two collections
      M(x ? [a, b, c] : [d, e, f]);
      
      // A null conditional safely indexing into 'x':
      M(x ? [a, b, c]);
      

      Sans lookahead complexe, il serait impossible de le savoir sans consommer l'intégralité du littéral.

      Remarque : ce problème se pose même en l'absence de type naturel, car le typage cible s'applique par l'intermédiaire de conditional_expressions.

      Comme avec les autres, nous pourrions exiger des parenthèses pour lever l’ambiguïté. En d’autres termes, présumez l’interprétation null_conditional_operation, sauf s’il est écrit comme suit : x ? ([1, 2, 3]) :. Cependant, cela semble plutôt malheureux. Il est probable que ce type de code ne soit pas déraisonnable à écrire, mais cela pourrait tromper les gens.

Inconvénients

  • Cela introduit encore une autre forme pour les expressions de collection en plus de la myriade de façons dont nous disposons déjà. Il s’agit d’une complexité supplémentaire pour la langue. Cela dit, il est également possible d'unifier la syntaxe en un seul anneau, ce qui signifie que les bases de code existantes peuvent être simplifiées et déplacées vers un aspect uniforme partout.
  • L’utilisation de [...] au lieu de {...} s’éloigne de la syntaxe que nous avons généralement utilisée pour les tableaux et les initialiseurs de collection déjà. Plus précisément qu’il utilise [...] au lieu de {...}. Toutefois, cela a déjà été réglé par l’équipe linguistique lorsque nous avons fait la liste des modèles. Nous avons tenté de faire {...} travailler avec des modèles de liste et avons rencontré des problèmes insurmontables. En raison de cela, nous sommes passés à [...] qui, bien que nouveau pour C#, se sent naturel dans de nombreux langages de programmation et nous a permis de commencer frais sans ambiguïté. L’utilisation de [...] comme forme littérale correspondante est complémentaire avec nos dernières décisions, et nous donne un endroit propre pour travailler sans problème.

Cela introduit des verrues dans la langue. Par exemple, les éléments suivants sont juridiques et (heureusement) signifient exactement la même chose :

int[] x = { 1, 2, 3 };
int[] x = [ 1, 2, 3 ];

Toutefois, étant donné l’étendue et la cohérence apportées par la nouvelle syntaxe littérale, nous devrions envisager de recommander que les personnes passent au nouveau formulaire. Les suggestions et correctifs de l’IDE peuvent vous aider à cet égard.

Alternatives

  • Quelles autres conceptions ont été prises en compte ? Quel est l’impact de ne pas le faire ?

Questions résolues

  • Le compilateur doit-il utiliser stackalloc pour l’allocation de pile lorsque tableaux inline ne sont pas disponibles et que le type d’itération est un type primitif ?

    Résolution : Non. La gestion d’une mémoire tampon stackalloc nécessite un effort supplémentaire sur un tableau inline pour vous assurer que la mémoire tampon n’est pas allouée à plusieurs reprises lorsque l’expression de collection se trouve dans une boucle. La complexité supplémentaire dans le compilateur et dans le code généré l’emporte sur l’avantage de l’allocation de pile sur les plateformes plus anciennes.

  • Dans quel ordre devons-nous évaluer les éléments littéraux par rapport à l'évaluation des propriétés Length/Count ? Devons-nous d’abord évaluer tous les éléments, puis toutes les longueurs ? Ou devrions-nous évaluer un élément, puis sa longueur, puis l’élément suivant, et ainsi de suite ?

    Résolution : Nous évaluons tout d’abord tous les éléments, puis tout le reste suit cela.

  • Un élément littéral de longueur inconnue peut-il créer un type de collection qui nécessite une longueur connue, comme un tableau, une étendue ou une collection Construct(tableau/étendue) ? Il serait plus difficile de le faire efficacement, mais cela pourrait être possible grâce à une utilisation intelligente des tableaux poolés et/ou des constructeurs.

    Résolution : Oui, nous autorisons la création d'une collection de longueur fixe à partir d'un élément littéral de longueur inconnue. Le compilateur est autorisé à implémenter cela de manière aussi efficace que possible.

    Le texte suivant existe pour enregistrer la discussion d’origine de cette rubrique.

    Les utilisateurs peuvent toujours transformer un littéral de longueur inconnue en un de longueur connue avec du code comme celui-ci :

    ImmutableArray<int> x = [a, ..unknownLength.ToArray(), b];
    

    Toutefois, cela est malheureux en raison de la nécessité de forcer les allocations de stockage temporaire. Nous pourrions potentiellement être plus efficaces si nous avons contrôlé la façon dont cela a été émis.

  • Un collection_expression peut-il être ciblé sur un IEnumerable<T> ou sur d'autres interfaces de collection ?

    Par exemple:

    void DoWork(IEnumerable<long> values) { ... }
    // Needs to produce `longs` not `ints` for this to work.
    DoWork([1, 2, 3]);
    

    Résolution : Oui, un littéral peut être ciblé sur n'importe quel type d'interface I<T> que List<T> met en œuvre. Par exemple, IEnumerable<long>. Cela revient à cibler List<long> et à affecter le résultat au type d'interface spécifié. Le texte suivant existe pour enregistrer la discussion d’origine de cette rubrique.

    La question ouverte ici consiste à déterminer le type sous-jacent à créer. Une option consiste à examiner la proposition relative à params IEnumerable<T>. Là, nous générerions un tableau pour transmettre les valeurs, comme ce qui se passe avec params T[].

  • Le compilateur peut-il émettre le Array.Empty<T>() pour []? Devrions-nous le faire pour éviter les allocations dans la mesure du possible ?

    Oui. Le compilateur doit émettre Array.Empty<T>() pour tout cas où il s’agit d’un résultat légal et que le résultat final n’est pas mutable. Par exemple, cibler T[], IEnumerable<T>, IReadOnlyCollection<T> ou IReadOnlyList<T>. Il ne doit pas utiliser Array.Empty<T> lorsque la cible est mutable (ICollection<T> ou IList<T>).

  • Devrions-nous étendre les initialisateurs de collections à la très courante méthode AddRange ? Il peut être utilisé par le type construit sous-jacent pour effectuer l’ajout d’éléments répartis potentiellement plus efficacement. Nous pourrions également vouloir rechercher des choses comme .CopyTo. Il peut y avoir des inconvénients ici, car ces méthodes peuvent entraîner des allocations/répartitions excédentaires par rapport à l’énumération directe dans le code traduit.

    Oui. Une implémentation est autorisée à utiliser d’autres méthodes pour initialiser une valeur de collection, sous la présomption que ces méthodes ont une sémantique bien définie et que les types de collection doivent être « bien comportementés ». Dans la pratique, cependant, une implémentation devrait être prudente car les avantages d'une manière (copie en masse) peuvent avoir des conséquences négatives (par exemple, la mise en boîte d'une collection struct).

    Une implémentation doit tirer parti dans les cas où il n’y a aucun inconvénient. Par exemple, avec une méthode .AddRange(ReadOnlySpan<T>).

Questions non résolues

  • Devrions-nous autoriser l’inférence du type d’élément lorsque le type d’itération est « ambigu » (par une définition) ? Par exemple:
Collection x = [1L, 2L];

// error CS1640: foreach statement cannot operate on variables of type 'Collection' because it implements multiple instantiations of 'IEnumerable<T>'; try casting to a specific interface instantiation
foreach (var x in new Collection) { }

static class Builder
{
    public Collection Create(ReadOnlySpan<long> items) => throw null;
}

[CollectionBuilder(...)]
class Collection : IEnumerable<int>, IEnumerable<string>
{
    IEnumerator<int> IEnumerable<int>.GetEnumerator() => throw null;
    IEnumerator<string> IEnumerable<string>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}
  • Est-il légal de créer et d'indexer immédiatement une collection littérale ? Note : ceci nécessite une réponse à la question non résolue ci-dessous de savoir si les littéraux de collection ont un type naturel.

  • Les allocations de pile pour les grandes collections peuvent faire exploser la pile. Le compilateur devrait-il disposer d'une heuristique pour placer ces données sur le tas ? La langue doit-elle être non spécifiée pour permettre cette flexibilité ? Nous devrions suivre la spécification de params Span<T>.

  • Avons-nous besoin d'un type cible spread_element? Prenons l’exemple suivant :

    Span<int> span = [a, ..b ? [c] : [d, e], f];
    

    Remarque : cela peut généralement apparaître sous la forme suivante pour autoriser l’inclusion conditionnelle d’un ensemble d’éléments, ou rien si la condition est false :

    Span<int> span = [a, ..b ? [c, d, e] : [], f];
    

    Afin d'évaluer ce littéral complet, nous devons évaluer les expressions des éléments qu'il contient. Cela signifie qu'il faut être capable d'évaluer b ? [c] : [d, e]. Toutefois, en l’absence d’un type cible pour évaluer cette expression dans le contexte, et en l’absence d’une sorte de type naturel, nous ne pourrions pas déterminer ce qu’il faut faire avec [c] ou [d, e] ici.

    Pour résoudre ce problème, nous pourrions dire que lors de l’évaluation de l’expression spread_element d’un littéral, il y avait un type cible implicite équivalent au type cible du littéral lui-même. Ainsi, dans la version ci-dessus, cela serait réécrit comme suit :

    int __e1 = a;
    Span<int> __s1 = b ? [c] : [d, e];
    int __e2 = f;
    
    Span<int> __result = stackalloc int[2 + __s1.Length];
    int __index = 0;
    
    __result[__index++] = a;
    foreach (int __t in __s1)
      __result[index++] = __t;
    __result[__index++] = f;
    
    Span<int> span = __result;
    

La spécification d'un type de collection constructible à l'aide d'une méthode de création est sensible au contexte dans lequel la conversion est classée

L’existence de la conversion dans ce cas dépend de la notion d’un type d’itération du type de collection . S'il existe une méthode de création qui prend un ReadOnlySpan<T>T est le type d'itération, la conversion existe. Dans le cas contraire, elle n'existe pas.

Cependant, un type d'itération est sensible au contexte dans lequel foreach est exécuté. Pour le même type de collection , il peut être différent, selon les méthodes d'extension appliquées, et il peut également être indéfini.

Cela ne pose pas de problème pour l'objectif de foreach lorsque le type n'est pas conçu pour être accessible à l'avance sur lui-même. Si c'est le cas, les méthodes d'extension ne peuvent pas modifier la façon dont le type est atteint, quel que soit le contexte.

Toutefois, cela semble un peu étrange pour une conversion d’être sensible au contexte comme cela. En réalité, la conversion est « instable ». Un type de collection , explicitement conçu pour être constructible, peut ne pas inclure la définition d’un détail très important : son type d’itération . Laissant le type « inconvertible » tel quel.

Voici un exemple :

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

[CollectionBuilder(typeof(MyCollectionBuilder), nameof(MyCollectionBuilder.Create))]
class MyCollection
{
}
class MyCollectionBuilder
{
    public static MyCollection Create(ReadOnlySpan<long> items) => throw null;
    public static MyCollection Create(ReadOnlySpan<string> items) => throw null;
}

namespace Ns1
{
    static class Ext
    {
        public static IEnumerator<long> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                long s = l;
            }
        
            MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                               2];
        }
    }
}

namespace Ns2
{
    static class Ext
    {
        public static IEnumerator<string> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                string s = l;
            }
        
            MyCollection x1 = ["a",
                               2]; // error CS0029: Cannot implicitly convert type 'int' to 'string'
        }
    }
}

namespace Ns3
{
    class Program
    {
        static void Main()
        {
            // error CS1579: foreach statement cannot operate on variables of type 'MyCollection' because 'MyCollection' does not contain a public instance or extension definition for 'GetEnumerator'
            foreach (var l in new MyCollection())
            {
            }
        
            MyCollection x1 = ["a", 2]; // error CS9188: 'MyCollection' has a CollectionBuilderAttribute but no element type.
        }
    }
}

Étant donné la conception actuelle, si le type ne définit pas type d’itération lui-même, le compilateur ne peut pas valider de manière fiable une application d’un attribut CollectionBuilder. Si nous ne connaissons pas le type d’itération , nous ne savons pas quelle signature de la méthode de création doit être. Si le type d’itération provient du contexte, il n’existe aucune garantie que le type va toujours être utilisé dans un contexte similaire.

La fonctionnalité des collections de paramètres est également concernée. Il semble étrange de ne pas pouvoir prédire de manière fiable le type d’élément d’un paramètre params au point de déclaration. La proposition actuelle exige également que la méthode de création soit au moins aussi accessible que le paramstype de collection. Il est impossible d’effectuer cette vérification de manière fiable, sauf si le type de collection définit son type d’itération lui-même.

Notez que nous avons également https://github.com/dotnet/roslyn/issues/69676 ouvert pour le compilateur, qui observe essentiellement le même problème, mais parle de celui-ci du point de vue de l’optimisation.

Proposition

Nécessiter un type utilisant l'attribut CollectionBuilder pour définir en lui-même son type d'itération . En d’autres termes, cela signifie que le type doit implémenter IEnumarable/IEnumerable<T>, ou qu’il doit avoir une méthode de GetEnumerator publique avec la signature appropriée (cela exclut toutes les méthodes d’extension).

De plus, à l'heure actuelle, la méthode de création doit « être accessible lorsque l'expression de la collection est utilisée ». Il s’agit d’un autre point de dépendance de contexte basé sur l’accessibilité. L’objectif de cette méthode est très similaire à l’objectif d’une méthode de conversion définie par l’utilisateur, et celui-ci doit être public. Par conséquent, nous devrions envisager d’exiger que la méthode de création soit également publique.

Conclusion

Approuvé avec les modifications LDM-2024-01-08

La notion de type d’itération n’est pas appliquée de manière cohérente dans conversions

  • À un type de structure ou de classe qui implémente System.Collections.Generic.IEnumerable<T> où :
    • Pour chaque élément Ei il existe une conversion implicite en T.

Il semble que l'on suppose que T est nécessairement le type d'itération de la structure ou de la classe dans ce cas. Toutefois, cette hypothèse est incorrecte. Ce qui peut entraîner un comportement très étrange. Par exemple:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public void Add(string l) => throw null;
    
    public IEnumerator<string> GetEnumerator() => throw null; 
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
        
        MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                           2];
        MyCollection x2 = new MyCollection() { "b" };
    }
}
  • À une structure ou une classe qui implémente System.Collections.IEnumerable et qui ne l'implémente pasSystem.Collections.Generic.IEnumerable<T>.

Il semble que l’implémentation suppose que le type d’itération est object, mais la spécification laisse ce fait non spécifié et ne nécessite simplement pas chaque élément pour convertir en quoi que ce soit. En général, toutefois, le type d’itération n’est pas nécessaire pour le type object. Ce qui peut être observé dans l’exemple suivant :

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    public IEnumerator<string> GetEnumerator() => throw null; 
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
    }
}

La notion de type d'itération est fondamentale pour la fonctionnalité Params Collections. Et ce problème entraîne une différence étrange entre les deux caractéristiques. Par exemple:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 

    public void Add(long l) => throw null; 
    public void Add(string l) => throw null; 
}

class Program
{
    static void Main()
    {
        Test("2"); // error CS0029: Cannot implicitly convert type 'string' to 'long'
        Test(["2"]); // error CS1503: Argument 1: cannot convert from 'collection expressions' to 'string'
        Test(3); // error CS1503: Argument 1: cannot convert from 'int' to 'string'
        Test([3]); // Ok

        MyCollection x1 = ["2"]; // error CS0029: Cannot implicitly convert type 'string' to 'long'
        MyCollection x2 = [3];
    }

    static void Test(params MyCollection a)
    {
    }
}
using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 
    public void Add(object l) => throw null;
}

class Program
{
    static void Main()
    {
        Test("2", 3); // error CS1503: Argument 2: cannot convert from 'int' to 'string'
        Test(["2", 3]); // Ok
    }

    static void Test(params MyCollection a)
    {
    }
}

Il sera probablement bon de s'aligner dans un sens ou dans l'autre.

Proposition

Spécifiez la convertibilité de la struct ou de la classe qui implémente System.Collections.Generic.IEnumerable<T> ou System.Collections.IEnumerable, en termes de type d’itération , et exige une conversion implicite pour chaque élément Ei vers le type d'itération .

Conclusion

Approuvé LDM-2024-01-08

La conversion des expressions de collection devrait-elle nécessiter la disponibilité d'un groupe à haute disponibilité d'API pour la construction ?

Un type de collection constructible selon les conversions peut en fait ne pas être constructible, ce qui est susceptible d'entraîner un comportement inattendu en matière de résolution des surcharges. Par exemple:

class C1
{
    public static void M1(string x)
    {
    }
    public static void M1(char[] x)
    {
    }
    
    void Test()
    {
        M1(['a', 'b']); // error CS0121: The call is ambiguous between the following methods or properties: 'C1.M1(string)' and 'C1.M1(char[])'
    }
}

Cependant, le « C1. M1(string)' n’est pas un candidat qui peut être utilisé, car :

error CS1729: 'string' does not contain a constructor that takes 0 arguments
error CS1061: 'string' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)

Voici un autre exemple avec un type défini par l’utilisateur et une erreur plus forte qui ne mentionne même pas un candidat valide :

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(C1 x)
    {
    }
    public static void M1(char[] x)
    {
    }

    void Test()
    {
        M1(['a', 'b']); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
    }

    public static implicit operator char[](C1 x) => throw null;
    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

Il semble que la situation soit très similaire à celle que nous avions avec les conversions de groupes de méthodes en délégués. C’est-à-dire qu’il y avait des scénarios où la conversion existait, mais qu’elle était erronée. Nous avons décidé d’améliorer cela en garantissant que, si la conversion est erronée, elle n’existe pas.

Notez que, avec la fonctionnalité « Params Collections », nous allons rencontrer un problème similaire. Il peut être judicieux d'interdire l'utilisation du modificateur params pour les collections non constructibles. Cependant, dans la proposition actuelle, cette vérification est basée sur la section des conversions. Voici un exemple :

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(params C1 x) // It is probably better to report an error about an invalid `params` modifier
    {
    }
    public static void M1(params ushort[] x)
    {
    }

    void Test()
    {
        M1('a', 'b'); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
        M2('a', 'b'); // Ok
    }

    public static void M2(params ushort[] x)
    {
    }

    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

On dirait que la question était quelque peu abordée précédemment, voir https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions. À cette époque, un argument a été avancé que les règles, telles qu'elles sont spécifiées actuellement, sont cohérentes avec la spécification des gestionnaires de chaînes interpolées. Voici une citation :

En particulier, les gestionnaires de chaînes interpolés ont été spécifiés à l’origine de cette façon, mais nous avons révisé la spécification après avoir examiné ce problème.

Bien qu’il y ait une certaine similarité, il existe également une distinction importante à prendre en compte. Voici une citation de https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion:

Le type T est considéré comme un applicable_interpolated_string_handler_type s’il est attribué avec System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Il existe une conversion implicite interpolated_string_handler_conversion vers T à partir d'une interpolated_string_expression, ou d'une additive_expression composée entièrement d'_interpolated_string_expression_s et n'utilisant que des opérateurs +.

Le type cible doit avoir un attribut spécial qui est un indicateur fort de l’intention de l’auteur pour que le type soit un gestionnaire de chaînes interpolé. Il est juste de supposer que la présence de l’attribut n’est pas une coïncidence. En revanche, le fait qu’un type soit « énumérable », ne signifie pas nécessaire qu’il y ait eu l’intention de l’auteur pour que le type soit constructible. Cependant, la présence d'une méthode de création, qui est indiquée par un attribut [CollectionBuilder(...)] sur le type collection, semble être un indicateur fort de l'intention de l'auteur de rendre le type constructible.

Proposition

Pour un type struct ou class qui implémente System.Collections.IEnumerable et qui n'a pas de méthode create, la section sur les conversions devrait exiger la présence d'au moins les API suivantes :

  • Constructeur accessible applicable sans argument.
  • Une instance Add accessible ou une méthode d'extension qui peut être invoquée avec la valeur du type d'itération comme argument.

Pour les besoins de la fonctionnalité des Collections de paramètres , ces types sont des types params valides lorsque ces API sont déclarées publiques et lorsqu'elles sont des méthodes d'instance (par opposition à des méthodes d'extension).

Conclusion

Approuvé avec les modifications LDM-2024-01-10

Concevoir des réunions

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-01.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-09.md#ambiguity-of--in-collection-expressions https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-28.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-08.md https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-10.md

Réunions de groupe de travail

https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-06.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-14.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-21.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-05.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-28.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-05-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-12.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-03.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-10.md

Éléments à venir de l’ordre du jour

  • Les allocations de pile pour les grandes collections peuvent faire exploser la pile. Le compilateur devrait-il disposer d'une heuristique pour placer ces données sur le tas ? La langue doit-elle être non spécifiée pour permettre cette flexibilité ? Nous devons suivre ce que fait la spécification/impl pour params Span<T>. Les options sont les suivantes :

    • Toujours utiliser stackalloc. Enseignez aux gens de faire attention à Span. Cela permet à des choses comme Span<T> span = [1, 2, ..s] de fonctionner et d'être correctes tant que s est petit. Si cela pouvait faire exploser la pile, les utilisateurs pourraient toujours créer un tableau à la place, et ainsi obtenir une étendue autour de ce problème. Cela semble le plus conforme à ce que les gens pourraient vouloir, mais avec un danger extrême.
    • N'utilisez le stackalloc que lorsque le littéral a un nombre fixe d'éléments (c'est-à-dire pas d'éléments étalés). Cela rend alors probablement les choses toujours sécurisées, avec une utilisation fixe de la pile, et le compilateur (espérons-le) capable de réutiliser cette mémoire tampon fixe. Toutefois, cela signifie que des choses comme [1, 2, ..s] ne seraient jamais possibles, même si l’utilisateur sait qu’il est complètement sécurisé au moment de l’exécution.
  • Comment fonctionne la résolution de surcharge ? Si une API a :

    public void M(T[] values);
    public void M(List<T> values);
    

    Que se passe-t-il avec M([1, 2, 3])? Nous devons probablement définir une « meilleure qualité » pour ces conversions.

  • Devons-nous élargir les initialiseurs de collection pour inclure la très courante méthode AddRange ? Il peut être utilisé par le type construit sous-jacent pour effectuer l’ajout d’éléments répartis potentiellement plus efficacement. Nous pourrions également vouloir rechercher des choses comme .CopyTo. Il peut y avoir des inconvénients ici, car ces méthodes peuvent entraîner des allocations/répartitions excédentaires par rapport à l’énumération directe dans le code traduit.

  • L'inférence de type générique doit être mise à jour pour permettre le flux d'informations de type vers/depuis les littéraux de collection. Par exemple:

    void M<T>(T[] values);
    M([1, 2, 3]);
    

    Il semble naturel que cela soit quelque chose dont l'algorithme d'inférence puisse être informé. Une fois que cela sera pris en charge pour les cas de type de collection constructible « de base » (T[], I<T>, Span<T>new T()), cela devrait également s'appliquer au cas Collect(constructible_type). Par exemple:

    void M<T>(ImmutableArray<T> values);
    M([1, 2, 3]);
    

    Ici, Immutable<T> est constructible par le biais d’une méthode init void Construct(T[] values). Ainsi, le type T[] values serait utilisé avec une inférence contre [1, 2, 3], ce qui conduirait à une inférence de int pour T.

  • Ambiguïté Cast/Index.

    Aujourd’hui, voici une expression indexée dans

    var v = (Expr)[1, 2, 3];
    

    Mais il serait agréable de pouvoir faire des choses comme :

    var v = (ImmutableArray<int>)[1, 2, 3];
    

    Pouvons/devrions-nous prendre une pause ici ?

  • Ambiguïtés syntactiques avec ?[.

    Il peut être utile de modifier les règles de nullable index access pour indiquer qu’aucun espace ne peut se produire entre ? et [. Il s'agirait d'un changement radical (mais probablement mineur, car VS les force déjà à se rejoindre si vous les tapez avec un espace). Si nous procédons ainsi, nous pouvons avoir x?[y] analysé différemment de x ? [y].

    Une chose similaire se produit si nous voulons opter pour https://github.com/dotnet/csharplang/issues/2926. Dans ce monde x?.y est ambigu avec x ? .y. Si nous exigeons que le ?. soit contigu, nous pouvons distinguer syntaxiquement les deux cas de manière triviale.