Compartir vía


Expresiones de colección

Nota:

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos y se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de idioma (LDM) pertinentes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones.

Problema planteado por el experto: https://github.com/dotnet/csharplang/issues/8652

Resumen

Las expresiones de colección presentan una nueva sintaxis terse, [e1, e2, e3, etc], para crear valores de colección comunes. Insertar otras colecciones en estos valores es posible usar un elemento ..e de propagación como el siguiente: [e1, ..c2, e2, ..c2].

Se pueden crear varios tipos similares a la colección sin necesidad de compatibilidad con BCL externa. Estos tipos son:

Hay compatibilidad adicional para los tipos similares a la colección que no se tratan en lo anterior a través de un nuevo atributo y patrón de API que se puede adoptar directamente en el propio tipo.

Motivación

  • Los valores similares a la colección están enormemente presentes en la programación, los algoritmos y, especialmente, en el ecosistema de C#/.NET. Casi todos los programas utilizarán estos valores para almacenar datos y enviar o recibir datos de otros componentes. Actualmente, casi todos los programas de C# deben usar muchos enfoques diferentes y desafortunadamente detallados para crear instancias de estos valores. Algunos enfoques también tienen desventajas de rendimiento. Estos son algunos ejemplos comunes:

    • Matrices, que requieren new Type[] o new[] antes de los { ... } valores.
    • Intervalos, que pueden usar stackalloc y otras construcciones complicadas.
    • Inicializadores de colección, que requieren sintaxis como new List<T> (falta de inferencia de un posible detalle T) antes de sus valores, y que pueden provocar varias reasignaciones de memoria porque usan N .Add invocaciones sin proporcionar una capacidad inicial.
    • Colecciones inmutables, que requieren sintaxis como ImmutableArray.Create(...) inicializar los valores, y que pueden provocar asignaciones intermedias y copia de datos. Las formas de construcción más eficientes (como ImmutableArray.CreateBuilder) son inexorables y siguen produciendo basura inevitable.
  • Si observamos el ecosistema circundante, también encontramos ejemplos en todas partes de la creación de listas que son más cómodos y agradables de usar. TypeScript, Dart, Swift, Elm, Python, etc. opta por una sintaxis concisa para este propósito, con un uso generalizado y un gran efecto. Las investigaciones cursores no han revelado problemas sustantivos que surjan en esos ecosistemas con la construcción de estos literales.

  • C# también ha agregado patrones de lista en C# 11. Este patrón permite la coincidencia y deconstrucción de valores similares a la lista mediante una sintaxis limpia e intuitiva. Sin embargo, a diferencia de casi todas las demás construcciones de patrón, esta sintaxis coincidente o deconstrucción carece de la sintaxis de construcción correspondiente.

  • Obtener el mejor rendimiento para construir cada tipo de colección puede ser complicado. Las soluciones sencillas suelen desperdiciar tanto la CPU como la memoria. Tener un formulario literal permite la máxima flexibilidad de la implementación del compilador para optimizar el literal para generar al menos un buen resultado, ya que un usuario podría proporcionar, pero con código simple. Con mucha frecuencia, el compilador podrá mejorar y la especificación tiene como objetivo permitir la implementación de grandes cantidades de margen en términos de estrategia de implementación para garantizar esto.

Se necesita una solución inclusiva para C#. Debe cumplir la gran mayoría de casse para los clientes en términos de los tipos y valores similares a la colección que ya tienen. También debe sentirse natural en el lenguaje y reflejar el trabajo realizado en la coincidencia de patrones.

Esto conduce a una conclusión natural de que la sintaxis debe ser como [e1, e2, e3, e-etc] o [e1, ..c2, e2], que corresponden a los equivalentes de patrón de [p1, p2, p3, p-etc] y [p1, ..p2, p3].

Diseño detallado

Se agregan las siguientes producciones gramaticales :

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
  ;

Los literales de colección son de tipo de destino.

Aclaraciones de especificación

  • Por motivos de brevedad, collection_expression se denominará "literal" en las secciones siguientes.

  • expression_element Normalmente, las instancias se denominarán e1, e_n, etc.

  • spread_element Normalmente, las instancias se denominarán ..s1, ..s_n, etc.

  • span type significa Span<T> o ReadOnlySpan<T>.

  • Los literales normalmente se mostrarán como [e1, ..s1, e2, ..s2, etc] para transmitir cualquier número de elementos en cualquier orden. Importantemente, este formulario se usará para representar todos los casos, como:

    • Literales vacíos []
    • Literales sin en expression_element ellos.
    • Literales sin en spread_element ellos.
    • Literales con ordenación arbitraria de cualquier tipo de elemento.
  • El tipo de iteración de ..s_n es el tipo de la variable de iteración determinado como si s_n se usaran como la expresión en iteración en un foreach_statement.

  • Las variables a partir __name de se usan para representar los resultados de la evaluación de name, almacenados en una ubicación para que solo se evalúe una vez. Por ejemplo __e1 , es la evaluación de e1.

  • List<T>, IEnumerable<T>, etc. hacen referencia a los tipos respectivos en el System.Collections.Generic espacio de nombres .

  • La especificación define una traducción del literal a las construcciones de C# existentes. De forma similar a la traducción de expresiones de consulta, el literal solo es legal si la traducción daría lugar a código legal. El propósito de esta regla es evitar tener que repetir otras reglas del lenguaje que están implícitas (por ejemplo, sobre la convertibilidad de expresiones cuando se asignan a ubicaciones de almacenamiento).

  • No se requiere una implementación para traducir literales exactamente como se especifica a continuación. Cualquier traducción es legal si se produce el mismo resultado y no hay diferencias observables en la producción del resultado.

    • Por ejemplo, una implementación podría traducir literales como [1, 2, 3] directamente a una new int[] { 1, 2, 3 } expresión que en sí mismo hornea los datos sin procesar en el ensamblado, elidiendo la necesidad __index de o una secuencia de instrucciones para asignar cada valor. Lo importante es que esto significa si algún paso de la traducción podría provocar una excepción en tiempo de ejecución que el estado del programa todavía se deja en el estado indicado por la traducción.
  • Las referencias a "asignación de pila" hacen referencia a cualquier estrategia que se asigne en la pila y no al montón. Importantemente, no implica ni requiere que esa estrategia sea a través del mecanismo real stackalloc . Por ejemplo, el uso de matrices insertadas también es un enfoque permitido y deseable para lograr la asignación de pila cuando esté disponible. Tenga en cuenta que en C# 12, las matrices insertadas no se pueden inicializar con una expresión de colección. Eso sigue siendo una propuesta abierta.

  • Se supone que las colecciones se comportan bien. Por ejemplo:

    • Se supone que el valor de Count en una colección generará ese mismo valor que el número de elementos cuando se enumeran.
    • Se supone que los tipos usados en esta especificación definida en el System.Collections.Generic espacio de nombres son libres de efectos secundarios. Por lo tanto, el compilador puede optimizar escenarios en los que estos tipos se pueden usar como valores intermedios, pero de lo contrario no se exponen.
    • Se supone que una llamada a algún miembro aplicable .AddRange(x) en una colección dará como resultado el mismo valor final que la iteración x y la adición de todos sus valores enumerados individualmente a la colección con .Add.
    • El comportamiento de los literales de colección con colecciones que no se comportan correctamente es indefinido.

Conversions

Una conversión de expresiones de colección permite convertir una expresión de colección en un tipo.

Existe una conversión de expresión de colección implícita desde una expresión de colección a los siguientes tipos:

  • Tipo de matrizT[] unidimensional, en cuyo caso el tipo de elemento es T
  • Tipo de intervalo:
    • System.Span<T>
    • System.ReadOnlySpan<T>
      En cuyo caso el tipo de elemento es T
  • Tipo con un método de creación adecuado, en cuyo caso el tipo de elemento es el tipo de iteración determinado a partir de un método de instancia o una interfaz enumerable, no de un GetEnumerator método de extensión
  • Un tipode clase o estructura que implementa dóndeSystem.Collections.IEnumerable:
    • El tipo tiene un constructor aplicable que se puede invocar sin argumentos y el constructor es accesible en la ubicación de la expresión de colección.

    • Si la expresión de colección tiene elementos, el tipo tiene una instancia o un método Add de extensión donde:

      • El método se puede invocar con un único argumento de valor.
      • Si el método es genérico, los argumentos de tipo se pueden deducir de la colección y el argumento .
      • El método es accesible en la ubicación de la expresión de colección.

      En cuyo caso el tipo de elemento es el tipo de iteración del tipo.

  • Un tipo de interfaz:
    • 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>
      En cuyo caso el tipo de elemento es T

La conversión implícita existe si el tipo tiene un tipoT de elemento donde para cada elementoEᵢ de la expresión de colección:

  • Si Eᵢ es un elemento de expresión, hay una conversión implícita de Eᵢ a T.
  • Si Eᵢ es un elemento ..Sᵢ, hay una conversión implícita del tipo de iteración de Sᵢ a T.

No hay ninguna conversión de expresión de colección de una expresión de colección a un tipo de matriz multidimensional.

Los tipos para los que hay una conversión de expresión de colección implícita de una expresión de colección son los tipos de destino válidos para esa expresión de colección.

Existen las siguientes conversiones implícitas adicionales desde una expresión de colección:

  • Para un tipoT? de valor que acepta valores NULL donde hay una conversión de expresión de colección de la expresión de colección a un tipo de Tvalor . La conversión es una conversión de expresión de colección a T seguida de una conversión implícita que acepta valores NULL de T a T?.

  • Para un tipo T de referencia en el que hay un método create asociado a T que devuelve un tipo U y una conversión de referencia implícita de U a T. La conversión es una conversión de expresión de colección a U seguida de una conversión de referencia implícita de U a .T

  • Para un tipo I de interfaz en el que hay un método create asociado a I que devuelve un tipo V y una conversión de conversión boxing implícita de V a I. La conversión es una conversión de expresión de colección a V seguida de una conversión de conversión boxing implícita de V a I.

Creación de métodos

Un método create se indica con un [CollectionBuilder(...)] atributo en el tipo de colección. El atributo especifica el tipo de generador y el nombre del método de un método que se va a invocar para construir una instancia del tipo de colección.

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; }
    }
}

El atributo se puede aplicar a , classstruct, ref structo interface. El atributo no se hereda aunque el atributo se puede aplicar a una base class o a .abstract class

El tipo de generador debe ser un valor no genérico class o struct.

En primer lugar, se determina el conjunto de métodos de creación aplicablesCM .
Consta de métodos que cumplen los siguientes requisitos:

  • El método debe tener el nombre especificado en el [CollectionBuilder(...)] atributo .
  • El método debe definirse directamente en el tipo de generador .
  • El método debe ser static.
  • El método debe ser accesible donde se usa la expresión de colección.
  • La aridad del método debe coincidir con la aridad del tipo de colección.
  • El método debe tener un único parámetro de tipo System.ReadOnlySpan<E>, pasado por valor.
  • Hay una conversión de identidad, conversión de referencia implícita o conversión boxing del tipo de valor devuelto del método al tipo de colección.

Los métodos declarados en tipos base o interfaces se omiten y no forman parte del CM conjunto.

Si el CM conjunto está vacío, el tipo de colección no tiene un tipo de elemento y no tiene un método create. No se aplica ninguno de los pasos siguientes.

Si solo un método entre los del CM conjunto tiene una conversión de identidad desde E al tipo de elemento del tipo de colección, es el método create para el tipo de colección. De lo contrario, el tipo de colección no tiene un método create.

Se notifica un error si el [CollectionBuilder] atributo no hace referencia a un método invocable con la firma esperada.

Para una expresión de colección con un tipo C<S0, S1, …> de destino en el que la declaraciónC<T0, T1, …> de tipo tiene un métodoB.M<U0, U1, …>() de generador asociado, los argumentos de tipo genérico del tipo de destino se aplican en orden (y desde el tipo contenedor más externo al más interno) al método builder.

El parámetro span del método create se puede marcar scoped explícitamente o [UnscopedRef]. Si el parámetro es implícita o explícitamente scoped, el compilador puede asignar el almacenamiento para el intervalo en la pila en lugar del montón.

Por ejemplo, un posible método create para ImmutableArray<T>:

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

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

Con el método create anterior, ImmutableArray<int> ia = [1, 2, 3]; se podría emitir como:

[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

Los elementos de una expresión de colección se evalúan en orden, de izquierda a derecha. Cada elemento se evalúa exactamente una vez y cualquier referencia adicional a los elementos hace referencia a los resultados de esta evaluación inicial.

Se puede iterar un elemento de propagación antes o después de evaluar los elementos subsiguientes de la expresión de colección.

Una excepción no controlada producida desde cualquiera de los métodos utilizados durante la construcción no se detectará y evitará pasos adicionales en la construcción.

LengthSe supone que , County GetEnumerator no tienen efectos secundarios.


Si el tipo de destino es un tipode clase o estructura que implementa System.Collections.IEnumerabley el tipo de destino no tiene un método create, la construcción de la instancia de colección es la siguiente:

  • Los elementos se evalúan en orden. Algunos o todos los elementos se pueden evaluar durante los pasos siguientes en lugar de antes.

  • El compilador puede determinar la longitud conocida de la expresión de colección invocando propiedades que se pueden contar (o propiedades equivalentes de interfaces o tipos conocidos) en cada expresión de elemento distribuido.

  • Se invoca el constructor que es aplicable sin argumentos.

  • Para cada elemento en orden:

    • Si el elemento es un elemento de expresión, se invoca la instancia o el método de extensión aplicables Add con la expresión de elemento como argumento. (A diferencia del comportamiento del inicializador de colección clásica, la evaluación de elementos y Add las llamadas no se intercalan necesariamente).
    • Si el elemento es un elemento de propagación , se usa una de las siguientes opciones:
      • Se invoca un método de extensión o instancia aplicable GetEnumerator en la expresión de elemento de propagación y para cada elemento del enumerador, se invoca la instancia o el método de extensión aplicables Add en la instancia de colección con el elemento como argumento. Si el enumerador implementa IDisposable, Dispose se llamará después de la enumeración, independientemente de las excepciones.
      • Se invoca una instancia o método de extensión aplicable AddRange en la instancia de colección con la expresión de elemento de propagación como argumento.
      • Se invoca un método de extensión o instancia aplicable CopyTo en la expresión de elemento de propagación con la instancia de colección y int el índice como argumentos.
  • Durante los pasos de construcción anteriores, se EnsureCapacity invocar una instancia o método de extensión aplicables una o varias veces en la instancia de colección con un int argumento de capacidad.


Si el tipo de destino es una matriz, un intervalo, un tipo con un método create o una interfaz, la construcción de la instancia de colección es la siguiente:

  • Los elementos se evalúan en orden. Algunos o todos los elementos se pueden evaluar durante los pasos siguientes en lugar de antes.

  • El compilador puede determinar la longitud conocida de la expresión de colección invocando propiedades que se pueden contar (o propiedades equivalentes de interfaces o tipos conocidos) en cada expresión de elemento distribuido.

  • Se crea una instancia de inicialización de la siguiente manera:

    • Si el tipo de destino es una matriz y la expresión de colección tiene una longitud conocida, se asigna una matriz con la longitud esperada.
    • Si el tipo de destino es un intervalo o un tipo con un método create y la colección tiene una longitud conocida, se crea un intervalo con la longitud esperada que hace referencia al almacenamiento contiguo.
    • De lo contrario, se asigna almacenamiento intermedio.
  • Para cada elemento en orden:

    • Si el elemento es un elemento de expresión, se invoca el indexador de instancia de inicialización para agregar la expresión evaluada en el índice actual.
    • Si el elemento es un elemento de propagación , se usa una de las siguientes opciones:
      • Se invoca a un miembro de una interfaz o un tipo conocidos para copiar elementos de la expresión de elemento de propagación a la instancia de inicialización.
      • Se invoca un método de extensión o instancia aplicable GetEnumerator en la expresión de elemento de propagación y para cada elemento del enumerador, se invoca el indexador de instancia de inicialización para agregar el elemento en el índice actual. Si el enumerador implementa IDisposable, Dispose se llamará después de la enumeración, independientemente de las excepciones.
      • Se invoca un método de extensión o instancia aplicable CopyTo en la expresión de elemento de propagación con la instancia de inicialización y int el índice como argumentos.
  • Si se asignó almacenamiento intermedio para la colección, se asigna una instancia de colección con la longitud real de la colección y los valores de la instancia de inicialización se copian en la instancia de colección, o si se requiere un intervalo, el compilador puede usar un intervalo de la longitud de la colección real del almacenamiento intermedio. De lo contrario, la instancia de inicialización es la instancia de colección.

  • Si el tipo de destino tiene un método create, se invoca el método create con la instancia de span.


Nota: El compilador puede retrasar la adición de elementos a la colección (o retrasar la iteración a través de elementos distribuidos) hasta después de evaluar los elementos posteriores. (Cuando los elementos de propagación posteriores tienen propiedades con recuento que permitirían calcular la longitud esperada de la colección antes de asignar la colección). Por el contrario, el compilador puede agregar elementos de forma diligente a la colección (y iterar diligentemente a través de elementos extendidos) cuando no hay ninguna ventaja de retrasar.

Tenga en cuenta la siguiente expresión de colección:

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

Si los elementos distribuidos b y c son countables, el compilador podría retrasar la adición de elementos desde a y b hasta después c de evaluarse, para permitir la asignación de la matriz resultante a la longitud esperada. Después de eso, el compilador podría agregar elementos de forma diligente desde , antes de cevaluar 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;

Literal de colección vacía

  • El literal [] vacío no tiene ningún tipo. Sin embargo, de forma similar al literal null, este literal se puede convertir implícitamente en cualquier tipo de colección construyeble .

    Por ejemplo, lo siguiente no es legal, ya que no hay ningún tipo de destino y no hay ninguna otra conversión implicada:

    var v = []; // illegal
    
  • La propagación de un literal vacío se permite elided. Por ejemplo:

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

    Aquí, si b es false, no es necesario que ningún valor se construya realmente para la expresión de colección vacía, ya que inmediatamente se distribuiría en cero valores en el literal final.

  • La expresión de colección vacía puede ser un singleton si se usa para construir un valor de colección final que se sabe que no es mutable. Por ejemplo:

    // 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 = [];
    

Seguridad de referencias

Consulte restricción de contexto seguro para obtener definiciones de los valores de contexto seguro : declaration-block, function-member y caller-context.

El contexto seguro de una expresión de colección es:

  • El contexto seguro de una expresión [] de colección vacía es el contexto del autor de la llamada.

  • Si el tipo de destino es un tipoSystem.ReadOnlySpan<T> de intervalo y T es uno de los tipos primitivosbool, sbyte, byteshortushortcharintuint, , long, ulongfloat, o double, y la expresión de colección contiene solo valores constantes, el contexto seguro de la expresión de colección es el contexto del autor de la llamada.

  • Si el tipo de destino es un tipo System.Span<T> o System.ReadOnlySpan<T>, el contexto seguro de la expresión de colección es el bloque de declaración.

  • Si el tipo de destino es un tipo de estructura ref con un método create, el contexto seguro de la expresión de colección es el contexto seguro de una invocación del método create donde la expresión de colección es el argumento span para el método.

  • De lo contrario, el contexto seguro de la expresión de colección es el contexto del autor de la llamada.

Una expresión de colección con un contexto seguro de bloque de declaración no puede escapar del ámbito envolvente y el compilador puede almacenar la colección en la pila en lugar del montón.

Para permitir que una expresión de colección para un tipo de estructura ref escape al bloque de declaración, puede ser necesario convertir la expresión a otro tipo.

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
}

Inferencia de tipos

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;

Las reglas de inferencia de tipos se actualizan de la manera siguiente.

Las reglas existentes para la primera fase se extraen en una nueva sección de inferencia de tipos de entrada y se agrega una regla a la inferencia de tipos de entrada y a la inferencia de tipos de salida para expresiones de colección.

11.6.3.2 La primera fase

Para cada uno de los argumentos Eᵢdel método :

  • Una inferencia de tipo de entrada se realiza desdeEᵢhasta el tipoTᵢ de parámetro correspondiente.

Una inferencia de tipo de entrada se realiza desde una expresión Ea un tipo T de la siguiente manera:

  • Si E es una expresión de colección con elementos Eᵢy T es un tipo con un tipoTₑ de elemento o T es un tipoT0? de valor que acepta valores NULL y T0 tiene un tipoTₑ de elemento, por cada Eᵢ:
    • Si Eᵢ es un elemento de expresión, se realiza una inferenciade tipo de entrada desdeEᵢaTₑ .
    • Si Eᵢ es un elemento de propagación con un tipo de Sᵢ, se realiza una inferencia de límite inferiordesdeSᵢaTₑ .
  • [reglas existentes de la primera fase] ...

11.6.3.7 Inferencias de tipo de salida

Una inferencia de tipo de salida se realiza desde una expresión Ea un tipo T de la siguiente manera:

  • Si E es una expresión de colección con elementos Eᵢy T es un tipo con un tipoTₑ de elemento o T es un tipoT0? de valor que acepta valores NULL y T0 tiene un tipoTₑ de elemento, por cada Eᵢ:
    • Si Eᵢ es un elemento de expresión, se realiza una inferenciade tipo de salida deEᵢa .Tₑ
    • Si Eᵢ es un elemento de propagación, no se realiza ninguna inferencia a partir de Eᵢ.
  • [reglas existentes de inferencias de tipo de salida] ...

Métodos de extensión

No hay cambios en las reglas de invocación del método de extensión .

12.8.10.3 Invocaciones a métodos de extensión

Un método Cᵢ.Mₑ de extensión es apto si:

  • ...
  • Existe una conversión implícita de identidad, referencia o conversión boxing de expr al tipo del primer parámetro de Mₑ.

Una expresión de colección no tiene un tipo natural, por lo que las conversiones existentes del tipo no son aplicables. Como resultado, una expresión de colección no se puede usar directamente como primer parámetro para una invocación de método de extensión.

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

Resolución de sobrecarga

Se actualiza una mejor conversión de la expresión para preferir determinados tipos de destino en las conversiones de expresiones de colección.

En las reglas actualizadas:

  • Una span_type es una de las siguientes:
    • System.Span<T>
    • System.ReadOnlySpan<T>.
  • Un array_or_array_interface es uno de los siguientes:
    • un tipo de matriz
    • uno de los siguientes tipos de interfaz implementados por un tipo de matriz:
      • 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>

Dada una conversión implícita C₁ que convierte de una expresión E a un tipo T₁, y una conversión implícita C₂ que convierte de una expresión E a un tipo T₂, C₁ es una conversión mejor que C₂ si se cumple una de las siguientes condiciones:

  • E es una expresión de colección y una de las siguientes suspensiones:
    • T₁ es System.ReadOnlySpan<E₁>, y T₂ es System.Span<E₂>y existe una conversión implícita de E₁ a . E₂
    • T₁ es System.ReadOnlySpan<E₁> o System.Span<E₁>, y T₂ es un array_or_array_interface con tipoE₂ de elemento y existe una conversión implícita de E₁ a . E₂
    • T₁ no es un span_type y T₂ no es un span_type y existe una conversión implícita de T₁ a . T₂
  • E no es una expresión de colección y una de las siguientes suspensiones:
    • E coincidencias T₁ exactas y E no coincide exactamente T₂
    • E coincide exactamente con o ninguno de T₁ y T₂, y T₁ es un destino de conversión mejor que T₂
  • E es un grupo de métodos, ...

Ejemplos de diferencias con la resolución de sobrecarga entre inicializadores de matriz y expresiones de colección:

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

Tipos de intervalo

Los tipos ReadOnlySpan<T> de intervalo y Span<T> son tipos de colección construyebles. La compatibilidad con ellos sigue el diseño de params Span<T>. En concreto, la construcción de cualquiera de esos intervalos dará como resultado una matriz T[] creada en la pila si la matriz de parámetros está dentro de los límites (si los hay) establecidos por el compilador. De lo contrario, la matriz se asignará en el montón.

Si el compilador decide asignar en la pila, no es necesario traducir un literal directamente a un stackalloc punto específico. Por ejemplo, dado:

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

El compilador puede traducirlo siempre stackalloc que el Span significado permanezca igual y se mantenga la seguridad de intervalos . Por ejemplo, puede traducir lo anterior a:

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
}

El compilador también puede usar matrices insertadas, si están disponibles, al elegir asignar en la pila. Tenga en cuenta que en C# 12, las matrices insertadas no se pueden inicializar con una expresión de colección. Esa característica es una propuesta abierta.

Si el compilador decide asignar en el montón, la traducción de Span<T> es simplemente:

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

Traducción literal de colección

Una expresión de colección tiene una longitud conocida si el tipo en tiempo de compilación de cada elemento de propagación de la expresión de colección es countable.

Traducción de interfaz

Traducción de interfaz no mutable

Dado un tipo de destino que no contiene miembros mutantes, es decirIEnumerable<T>IReadOnlyCollection<T>, , y IReadOnlyList<T>, se requiere una implementación compatible para generar un valor que implemente esa interfaz. Si se sintetiza un tipo, se recomienda que el tipo sintetizado implemente todas estas interfaces, así como ICollection<T> y IList<T>, independientemente del tipo de interfaz de destino. Esto garantiza la máxima compatibilidad con las bibliotecas existentes, incluidas las que introspeccionan las interfaces implementadas por un valor para iluminar las optimizaciones de rendimiento.

Además, el valor debe implementar las interfaces y ICollection no genéricosIList. Esto permite que las expresiones de colección admitan la introspección dinámica en escenarios como el enlace de datos.

Una implementación compatible es gratuita para:

  1. Use un tipo existente que implemente las interfaces necesarias.
  2. Sintetiza un tipo que implementa las interfaces necesarias.

En cualquier caso, el tipo usado puede implementar un conjunto mayor de interfaces que los estrictamente necesarios.

Los tipos sintetizados son libres de emplear cualquier estrategia que quieran implementar correctamente las interfaces necesarias. Por ejemplo, un tipo sintetizado podría insertar los elementos directamente dentro de sí, evitando la necesidad de asignaciones de colecciones internas adicionales. Un tipo sintetizado tampoco pudo usar ningún almacenamiento, optando por calcular los valores directamente. Por ejemplo, devolviendo index + 1 para [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].

  1. El valor debe devolverse true cuando se consulta ( ICollection<T>.IsReadOnly si se implementa) y no es IList.IsReadOnly genérico y IList.IsFixedSize. Esto garantiza que los consumidores puedan indicar adecuadamente que la colección no es mutable, a pesar de implementar las vistas mutables.
  2. El valor debe iniciarse en cualquier llamada a un método de mutación (como IList<T>.Add). Esto garantiza la seguridad, evitando que una colección no mutable se mute accidentalmente.

Traducción de interfaz mutable

Dado el tipo de destino que contiene miembros mutantes, es decir ICollection<T> , o IList<T>:

  1. El valor debe ser una instancia de List<T>.

Traducción de longitud conocida

Tener una longitud conocida permite una construcción eficaz de un resultado con la posibilidad de no copiar datos y sin espacio de demora innecesario en un resultado.

No tener una longitud conocida no impide que se cree ningún resultado. Sin embargo, puede dar lugar a costos adicionales de CPU y memoria que producen los datos y, a continuación, pasar al destino final.

  • Para un literal de [e1, ..s1, etc], la traducción comienza por lo siguiente:

    int __len = count_of_expression_elements +
                __s1.Count;
                ...
                __s_n.Count;
    
  • Dado un tipo T de destino para ese literal:

    • Si T es , T1[]el literal se traduce como:

      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
      

      La implementación puede utilizar otros medios para rellenar la matriz. Por ejemplo, utilizando métodos eficaces de copia masiva como .CopyTo().

    • Si T es , Span<T1>el literal se traduce como el mismo que antes, salvo que la __result inicialización se traduce como:

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

      La traducción puede usar stackalloc T1[] o una matriz insertada en lugar de new T1[] si se mantiene la seguridad del intervalo .

    • Si T es , ReadOnlySpan<T1>el literal se traduce igual que para el Span<T1> caso, salvo que el resultado final será que Span<T1>se convierta implícitamente en .ReadOnlySpan<T1>

      ReadOnlySpan<T1> Donde T1 es un tipo primitivo, y todos los elementos de colección son constantes no necesita que sus datos estén en el montón o en la pila. Por ejemplo, una implementación podría construir este intervalo directamente como referencia a una parte del segmento de datos del programa.

      Los formularios anteriores (para matrices y intervalos) son las representaciones base de la expresión de colección y se usan para las siguientes reglas de traducción:

      • Si T es algo C<S0, S1, …> que tiene un método create-methodB.M<U0, U1, …>() correspondiente, el literal se traduce como:

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

        Como el método create debe tener un tipo de argumento de algunas instancias ReadOnlySpan<T>de , la regla de traducción para intervalos se aplica al pasar la expresión de colección al método create.

      • Si T admite inicializadores de colección, haga lo siguiente:

        • si el tipo T contiene un constructor accesible con un único parámetro int capacity, el literal se traduce como:

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

          Nota: el nombre del parámetro debe ser capacity.

          Este formulario permite que un literal informe al tipo recién construido del recuento de elementos para permitir una asignación eficaz del almacenamiento interno. Esto evita reasignaciones desperdiciadas a medida que se agregan los elementos.

        • De lo contrario, el literal se traduce como:

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

          Esto permite crear el tipo de destino, aunque sin optimización de capacidad para evitar la reasignación interna del almacenamiento.

Traducción de longitud desconocida

  • Dado un tipo T de destino para un literal de longitud desconocida :

    • Si T admite inicializadores de colección, el literal se traduce como:

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

      Esto permite la propagación de cualquier tipo iterable, aunque con la menor cantidad de optimización posible.

    • Si T es , T1[]el literal tiene la misma semántica que:

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

      Sin embargo, lo anterior es ineficaz; crea la lista intermedia y, a continuación, crea una copia de la matriz final a partir de ella. Las implementaciones son gratuitas para optimizar esto, por ejemplo, produciendo código como el siguiente:

      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);
      

      Esto permite un mínimo desperdicio y copia, sin sobrecarga adicional que podrían incurrir las colecciones de bibliotecas.

      Los recuentos pasados a CreateArray se usan para proporcionar una sugerencia de tamaño inicial para evitar cambios de tamaño desperdiciados.

    • Si T es algún tipo de intervalo, una implementación puede seguir la estrategia anterior T[] o cualquier otra estrategia con la misma semántica, pero un mejor rendimiento. Por ejemplo, en lugar de asignar la matriz como una copia de los elementos de lista, CollectionsMarshal.AsSpan(__list) se podría usar para obtener un valor de intervalo directamente.

Escenarios no soportados

Aunque los literales de colección se pueden usar para muchos escenarios, hay algunos que no son capaces de reemplazar. Estos incluyen:

  • Matrices multidimensionales (por ejemplo, new int[5, 10] { ... }). No hay ninguna instalación para incluir las dimensiones y todos los literales de colección son solo estructuras lineales o de mapa.
  • Colecciones que pasan valores especiales a sus constructores. No hay ninguna instalación para acceder al constructor que se está usando.
  • Inicializadores de colección anidados, por ejemplo, new Widget { Children = { w1, w2, w3 } }. Este formulario debe permanecer porque tiene una semántica muy diferente de Children = [w1, w2, w3]. El primero llama .Add repetidamente mientras .Children que el segundo asignaría una nueva colección a través .Childrende . Podríamos considerar que el último formulario se revierte a la adición a una colección existente si .Children no se puede asignar, pero parece que podría ser extremadamente confuso.

Ambigüedades de sintaxis

  • Hay dos ambigüedades sintácticas "verdaderas" en las que hay varias interpretaciones sintácticas legales del código que usa un collection_literal_expression.

    • es spread_element ambiguo con .range_expression Técnicamente podría tener:

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

      Para resolver esto, podemos:

      • Requerir que los usuarios entre paréntesis o (..e) incluyan un índice 0..e de inicio si quieren un intervalo.
      • Elija una sintaxis diferente (como ...) para la propagación. Esto sería lamentable por la falta de coherencia con los patrones de segmento.
  • Hay dos casos en los que no hay una verdadera ambigüedad, pero donde la sintaxis aumenta considerablemente la complejidad del análisis. Aunque no es un problema dado el tiempo de ingeniería, esto sigue aumentando la sobrecarga cognitiva para los usuarios al examinar el código.

    • Ambigüedad entre collection_literal_expression y attributes en instrucciones o funciones locales. Piense en lo siguiente:

      [X(), Y, Z()]
      

      Esto podría ser uno de los siguientes:

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

      Sin la apariencia compleja, sería imposible de decir sin consumir toda la totalidad del literal.

      Entre las opciones para abordar esto se incluyen:

      • Permita esto, realizando el trabajo de análisis para determinar cuál de estos casos es este.
      • No permitir esto y requerir que el usuario encapsula el literal entre paréntesis, como ([X(), Y, Z()]).ForEach(...).
      • Ambigüedad entre en collection_literal_expression y conditional_expression .null_conditional_operations Piense en lo siguiente:
      M(x ? [a, b, c]
      

      Esto podría ser uno de los siguientes:

      // 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]);
      

      Sin la apariencia compleja, sería imposible de decir sin consumir toda la totalidad del literal.

      Nota: este es un problema incluso sin un tipo natural porque la escritura de destino se aplica a través de conditional_expressions.

      Al igual que con los demás, podríamos requerir paréntesis para desambiguar. En otras palabras, supongamos la null_conditional_operation interpretación a menos que se escriba así: x ? ([1, 2, 3]) :. Sin embargo, eso parece bastante lamentable. Este tipo de código no parece poco razonable escribir y probablemente se subirá a las personas.

Inconvenientes

  • Esto presenta otra forma para las expresiones de colección sobre las innumerables formas que ya tenemos. Esto es una complejidad adicional para el lenguaje. Dicho esto también permite unificar en una sintaxis de anillo para gobernarlos todos, lo que significa que los códigos base existentes se pueden simplificar y mover a un aspecto uniforme en todas partes.
  • El uso [de ...] en lugar de {...} se aleja de la sintaxis que generalmente hemos usado para matrices y inicializadores de colección. En concreto, usa [...] en lugar de {...}. Sin embargo, el equipo de idioma ya lo estableció cuando hicimos una lista de patrones. Hemos intentado hacer {que ...} funcione con patrones de lista y se han encontrado problemas insurables. Debido a esto, nos mudamos a [...] que, mientras que el nuevo para C#, se siente natural en muchos lenguajes de programación y nos permitió empezar fresco sin ambigüedades. El uso [de ...] como forma literal correspondiente es complementario con nuestras decisiones más recientes, y nos da un lugar limpio para trabajar sin problema.

Esto introduce las warts en el idioma. Por ejemplo, los siguientes son legales y (afortunadamente) significan exactamente lo mismo:

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

Sin embargo, dada la amplitud y la coherencia que aporta la nueva sintaxis literal, debemos considerar la posibilidad de recomendar que las personas se muevan al nuevo formulario. Las sugerencias y correcciones del IDE podrían ayudar en ese sentido.

Alternatives

  • ¿Qué otros diseños se han considerado? ¿Cuál es el impacto de no hacer esto?

Preguntas resueltas

  • ¿Debe usar stackalloc el compilador para la asignación de pila cuando las matrices insertadas no están disponibles y el tipo de iteración es un tipo primitivo?

    Resolución: No. La administración de un stackalloc búfer requiere un esfuerzo adicional sobre una matriz insertada para asegurarse de que el búfer no se asigna repetidamente cuando la expresión de colección está dentro de un bucle. La complejidad adicional en el compilador y en el código generado supera la ventaja de la asignación de pila en plataformas anteriores.

  • ¿En qué orden deberíamos evaluar los elementos literales en comparación con la evaluación de propiedades Length/Count? ¿Deberíamos evaluar primero todos los elementos y, a continuación, todas las longitudes? ¿O deberíamos evaluar un elemento, luego su longitud, el siguiente elemento, etc.

    Resolución: primero se evalúan todos los elementos y, a continuación, todo lo demás sigue.

  • ¿Puede un literal de longitud desconocida crear un tipo de colección que necesite una longitud conocida, como una matriz, un intervalo o una colección Construct(array/span)? Esto sería más difícil de hacer de forma eficaz, pero podría ser posible mediante el uso inteligente de matrices agrupadas o generadores.

    Resolución: Sí, se permite crear una colección de longitud fija a partir de un literal de longitud desconocida . El compilador puede implementarlo de la manera más eficaz posible.

    El texto siguiente existe para registrar la discusión original de este tema.

    Los usuarios siempre podrían convertir un literal de longitud desconocida en una longitud conocida con código similar al siguiente:

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

    Sin embargo, esto es lamentable debido a la necesidad de forzar asignaciones de almacenamiento temporal. Podríamos ser más eficientes si controlamos cómo se emitió.

  • ¿Se puede escribir un objeto collection_expression de destino en una IEnumerable<T> u otras interfaces de colección?

    Por ejemplo:

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

    Resolución: Sí, un literal se puede escribir como destino en cualquier tipo I<T> de interfaz que List<T> implemente. Por ejemplo: IEnumerable<long>. Esto es lo mismo que la escritura de destino en List<long> y, a continuación, asigna ese resultado al tipo de interfaz especificado. El texto siguiente existe para registrar la discusión original de este tema.

    La pregunta abierta aquí es determinar qué tipo subyacente se va a crear realmente. Una opción es examinar la propuesta de params IEnumerable<T>. Allí, generaríamos una matriz para pasar los valores, de forma similar a lo que sucede con params T[].

  • ¿Puede o debe emitir Array.Empty<T>() el compilador para []? ¿Debemos exigir que lo haga para evitar asignaciones siempre que sea posible?

    Sí. El compilador debe emitir Array.Empty<T>() para cualquier caso en el que sea legal y el resultado final no sea mutable. Por ejemplo, como destino T[], IEnumerable<T>IReadOnlyCollection<T> o IReadOnlyList<T>. No debe usarse Array.Empty<T> cuando el destino es mutable (ICollection<T> o IList<T>).

  • ¿Deberíamos expandir los inicializadores de colección para buscar el método muy común AddRange ? Podría ser utilizado por el tipo construido subyacente para realizar la adición de elementos distribuidos potencialmente más eficientemente. También podríamos querer buscar cosas como .CopyTo . Puede haber inconvenientes aquí, ya que esos métodos podrían acabar causando excesos de asignaciones o envíos frente a enumerar directamente en el código traducido.

    Sí. Una implementación puede utilizar otros métodos para inicializar un valor de colección, bajo la presunción de que estos métodos tienen semántica bien definida y que los tipos de colección deben "comportarse bien". Sin embargo, en la práctica, una implementación debe tener cuidado, ya que las ventajas de una manera (copia masiva) también pueden tener consecuencias negativas (por ejemplo, la conversión boxing de una colección de estructuras).

    Una implementación debe aprovechar las ventajas en los casos en los que no hay inconvenientes. Por ejemplo, con un .AddRange(ReadOnlySpan<T>) método .

Preguntas sin resolver

  • ¿Deberíamos permitir la inferencia del tipo de elemento cuando el tipo de iteración es "ambiguo" (por alguna definición)? Por ejemplo:
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;
}
  • ¿Debe ser legal crear e indexar inmediatamente en un literal de colección? Nota: Esto requiere una respuesta a la pregunta sin resolver a continuación de si los literales de colección tienen un tipo natural.

  • Las asignaciones de pila para colecciones enormes podrían volar la pila. ¿El compilador debe tener una heurística para colocar estos datos en el montón? ¿Debe no especificarse el idioma para permitir esta flexibilidad? Deberíamos seguir las especificaciones de params Span<T>.

  • ¿Necesitamos tipo de destino spread_element? Tenga en cuenta, por ejemplo:

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

    Nota: esto puede aparecer normalmente en el siguiente formato para permitir la inclusión condicional de algunos conjuntos de elementos, o nada si la condición es falsa:

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

    Para evaluar este literal completo, es necesario evaluar las expresiones de elemento dentro. Esto significa poder evaluar b ? [c] : [d, e]. Sin embargo, si no hay un tipo de destino para evaluar esta expresión en el contexto de , y no hay ningún tipo de tipo natural, esto no podríamos determinar qué hacer con o [c][d, e] aquí.

    Para resolver esto, podríamos decir que al evaluar la expresión de spread_element un literal, había un tipo de destino implícito equivalente al tipo de destino del propio literal. Por lo tanto, en lo anterior, eso se reescribiría como:

    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 especificación de un tipo de colección construyeble que usa un método create es sensible al contexto en el que se clasifica la conversión.

Una existencia de la conversión en este caso depende de la noción de un tipo de iteración del tipo de colección. Si hay un método create que toma un ReadOnlySpan<T> donde T es el tipo de iteración, la conversión existe. De lo contrario, no.

Sin embargo, un tipo de iteración es sensible al contexto en el que foreach se realiza. Para el mismo tipo de colección , puede ser diferente en función de qué métodos de extensión están en el ámbito y también puede ser indefinido.

Eso se siente bien para el propósito de foreach cuando el tipo no está diseñado para ser capaz de ser capaz en sí mismo. Si es así, los métodos de extensión no pueden cambiar el modo en que se supera el tipo, independientemente del contexto.

Sin embargo, eso se siente algo extraño para que una conversión sea contextual como esa. De hecho, la conversión es "inestable". Un tipo de colección diseñado explícitamente para que se pueda construir puede dejar fuera una definición de un detalle muy importante: su tipo de iteración. Dejando el tipo "inconvertible" en sí mismo.

Este es un ejemplo:

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.
        }
    }
}

Dado el diseño actual, si el tipo no define el propio tipo de iteración , el compilador no puede validar de forma confiable una aplicación de un CollectionBuilder atributo. Si no conocemos el tipo de iteración, no sabemos cuál debe ser la firma del método create . Si el tipo de iteración procede del contexto, no hay ninguna garantía de que el tipo siempre se usará en un contexto similar.

La característica Recopilaciones de parámetros también se ve afectada por esto. Parece extraño no poder predecir de forma confiable el tipo de elemento de un params parámetro en el punto de declaración. La propuesta actual también requiere asegurarse de que el método create sea al menos tan accesible como el tipo de paramscolección. Es imposible realizar esta comprobación de forma confiable, a menos que el tipo de colección defina su propio tipo de iteración .

Tenga en cuenta que también hemos https://github.com/dotnet/roslyn/issues/69676 abierto para el compilador, que básicamente observa el mismo problema, pero se habla de él desde la perspectiva de la optimización.

Propuesta

Requerir un tipo que use CollectionBuilder el atributo para definir su tipo de iteración en sí mismo. Es decir, esto significa que el tipo debe implementar IEnumarable/IEnumerable<T>o debe tener un método público GetEnumerator con la firma correcta (esto excluye los métodos de extensión).

Además, ahora mismo es necesario crear método para "ser accesible donde se usa la expresión de colección". Este es otro punto de dependencia de contexto basada en la accesibilidad. El propósito de este método es muy similar al propósito de un método de conversión definido por el usuario y que debe ser público. Por lo tanto, debemos considerar la posibilidad de requerir que el método create también sea público.

Conclusión

Aprobado con modificaciones LDM-2024-01-08

La noción de tipo de iteración no se aplica de forma coherente en todas las conversiones.

  • Para un tipode clase o estructura que implementa dóndeSystem.Collections.Generic.IEnumerable<T>:
    • Para cada elementoEi hay una conversión implícita a T.

Parece que se supone que T es necesario el tipo de iteración de la estructura o el tipo de clase en este caso. Sin embargo, esa suposición es incorrecta. Lo que puede conducir a un comportamiento muy extraño. Por ejemplo:

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" };
    }
}
  • Para un tipode clase o estructura que implementa System.Collections.IEnumerable y no implementaSystem.Collections.Generic.IEnumerable<T>.

Parece que la implementación supone que el tipo de iteración es object, pero la especificación deja este hecho sin especificar y simplemente no requiere que cada elemento se convierta en nada. Sin embargo, en general, el tipo de iteración no es necesario.object Lo que se puede observar en el ejemplo siguiente:

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 noción de tipo de iteración es fundamental para la característica Colecciones de parámetros . Y este problema provoca una discrepancia extraña entre las dos características. Por ejemplo:

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)
    {
    }
}

Probablemente sea bueno alinearse de una manera u otra.

Propuesta

Especifique la convertibilidad del tipode clase o estructura que implementa o System.Collections.Generic.IEnumerable<T> en términos System.Collections.IEnumerable de tipo de iteración y requiere una conversión implícita para cada elementoEi al tipo de iteración.

Conclusión

LDM-2024-01-08 aprobado

¿Debe la conversión de expresiones de colección requerir disponibilidad de un conjunto mínimo de API para la construcción?

Un tipo de colección construyeble según las conversiones realmente no se puede construir, lo que es probable que lleve a un comportamiento inesperado de resolución de sobrecarga. Por ejemplo:

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[])'
    }
}

Sin embargo, el 'C1. M1(string)' no es un candidato que se puede usar porque:

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?)

Este es otro ejemplo con un tipo definido por el usuario y un error más seguro que ni siquiera menciona un candidato válido:

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;
}

Parece que la situación es muy similar a lo que solíamos tener con el grupo de métodos para delegar conversiones. Es decir, había escenarios en los que existía la conversión, pero era erróneo. Decidimos mejorarlo asegurándonos de que, si la conversión es errónea, entonces no existe.

Tenga en cuenta que, con la característica "Recopilaciones de parámetros", se producirá un problema similar. Puede ser conveniente no permitir el uso del params modificador para colecciones no construyebles. Sin embargo, en la propuesta actual, la comprobación se basa en la sección conversiones . Este es un ejemplo:

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;
}

Parece que el problema se ha analizado un poco anteriormente, vea https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions. En ese momento se hizo un argumento que las reglas, como se especifica en este momento, son coherentes con la forma en que se especifican los controladores de cadenas interpolados. Esta es una cita:

En concreto, los controladores de cadenas interpolados se especificaron originalmente de esta manera, pero revisamos la especificación después de considerar este problema.

Aunque hay cierta similitud, también hay una distinción importante que merece la pena tener en cuenta. Esta es una cita de https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion:

Se dice que el tipo T es un applicable_interpolated_string_handler_type si se atribuye a System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Existe un interpolated_string_handler_conversion implícito a T desde un interpolated_string_expression, o un additive_expression compuesto por completo de _interpolated_string_expression_s y usando solo + operadores.

El tipo de destino debe tener un atributo especial que sea un indicador seguro de la intención del autor para que el tipo sea un controlador de cadenas interpolado. Es justo suponer que la presencia del atributo no es una coincidencia. Por el contrario, el hecho de que un tipo sea "enumerable", no es necesario significa que haya la intención del autor para que el tipo se pueda construir. Sin embargo, una presencia de un método create, que se indica con un [CollectionBuilder(...)] atributo en el tipo de colección, se siente como un indicador seguro de la intención del autor para que el tipo sea construyeble.

Propuesta

Para un tipode clase o estructura que implementa y que no tiene una sección de System.Collections.IEnumerable de método de creación debe requerir presencia de al menos las SIGUIENTES API:

  • Constructor accesible que es aplicable sin argumentos.
  • Una instancia o método de extensión accesible Add que se puede invocar con el valor del tipo de iteración como argumento.

Para el propósito de la característica Dems Collectons , estos tipos son tipos válidos params cuando estas API se declaran públicas y son métodos de instancia (frente a extensión).

Conclusión

Aprobado con modificaciones LDM-2024-01-10