Compartir a través de


Argumentos de expresión de colección

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

Motivación

La característica de expresión de diccionario ha identificado la necesidad de que las expresiones de colección pasen los datos especificados por el usuario para configurar el comportamiento de la colección final. En concreto, los diccionarios permiten a los usuarios personalizar cómo se comparan sus claves, usándolos para definir la igualdad entre claves y la ordenación o el hash (en el caso de colecciones ordenadas o hash, respectivamente). Esta necesidad se aplica al crear cualquier tipo de tipo de diccionario (como D d = new D(...), D d = D.CreateRange(...) e incluso IDictionary<...> d = <synthesized dict>)

Para admitir esto, se propone un nuevo with(...arguments...) elemento como primer elemento de una expresión de colección de este modo:

Dictionary<string, int> nameToAge = [with(comparer), .. d1, .. d2, .. d3];
  1. Al traducir a una new CollectionType(...) llamada, se ...arguments... usan para determinar el constructor adecuado y se pasan en consecuencia.
  2. Al traducir a una CollectionFactory.Create llamada, estos ...arguments... se pasan antes con el ReadOnlySpan<ElementType> argumento elements, todos los cuales se usan para determinar la sobrecarga adecuada Create y se pasan en consecuencia.
  3. Al traducir a una interfaz (como IDictionary<,>) solo se permite un único argumento. Implementa una de las interfaces de comparador de BCL conocidas y se usará para controlar la clave que compara la semántica de la instancia final.

Esta sintaxis se eligió como:

  1. Mantiene toda la información dentro de la [...] sintaxis. Asegurarse de que el código sigue indicando claramente una colección que se está creando.
  2. No implica llamar a un new constructor (cuando no es cómo se crean todas las colecciones).
  3. No implica crear o copiar los valores de la colección varias veces (como podría ser un postfijo with { ... } .
  4. No contorniza el orden de las operaciones, especialmente con la semántica coherente de ordenación de la evaluación de expresiones de izquierda a derecha de C#. Por ejemplo, no evalúa los argumentos usados para construir una colección después de evaluar las expresiones usadas para rellenar la colección.
  5. No obliga a un usuario a leer al final de una expresión de colección (potencialmente grande) para determinar la semántica de comportamiento principal. Por ejemplo, tener que ver al final de un diccionario de cien líneas, solo para encontrar que, sí, estaba usando el comparador de claves correcto.
  6. Ambos no son sutiles, aunque tampoco son excesivamente detallados. Por ejemplo, usar ; en lugar de , para indicar argumentos es una parte muy sencilla de la sintaxis que se va a perder. with() solo agrega 6 caracteres y destacará fácilmente, especialmente con la coloración de sintaxis de la with palabra clave.
  7. Lee muy bien. "Se trata de una expresión de colección "con" estos argumentos, que consta de estos elementos".
  8. Resuelve la necesidad de comparadores para los diccionarios y conjuntos.
  9. Garantiza que cualquier necesidad de usuario para pasar argumentos, o cualquier necesidad que tengamos más allá de los comparadores en el futuro ya se controlan.
  10. No entra en conflicto con ningún código existente (usando https://grep.app/ para buscar).

Filosofía de diseño

En la sección siguiente se tratan los debates previos sobre la filosofía de diseño. Incluyendo por qué se rechazaron ciertos formularios.

Hay dos direcciones principales en las que podemos ir para proporcionar estos datos definidos por el usuario. La primera es para los valores de mayúsculas y minúsculas especiales en el espacio del comparador (que definimos como tipos que heredan de los tipos o IEqualityComparer<T> de IComparer<T> la BCL). La segunda consiste en proporcionar un mecanismo generalizado para proporcionar argumentos arbitrarios a la API invocada final al crear expresiones de colección. La especificación de expresión de diccionario principal muestra cómo podríamos hacer lo anterior, mientras que esta especificación busca hacer lo último.

Los exámenes de las soluciones para simplemente pasar comparadores han revelado debilidades en su enfoque si queremos expandirlas a argumentos arbitrarios. Por ejemplo:

  1. Reutilización de la sintaxis de elementos , como lo hacemos con el formato: [StringComparer.OrdinalIgnoreCase, "mads": 21]. Esto funciona bien en un espacio en el que KeyValuePair<,> los comparadores y no heredan de tipos comunes. Pero se rompe en un mundo donde uno podría hacer: HashSet<object> h = [StringComparer.OrdinalIgnoreCase, "v"]. ¿Esto pasa a lo largo de un comparador? ¿O intentar colocar dos valores de objeto en el conjunto?

  2. Separar argumentos frente a elementos con sintaxis sutil (como usar un punto y coma en lugar de una coma para separarlos en [comparer; v1]). Esto conlleva situaciones muy confusas [1; 2] en las que un usuario escribe accidentalmente (y obtiene una colección que pasa "1" como, por ejemplo, el argumento "capacity" para un List<>y solo contiene el valor único '2'), cuando se pretende [1, 2] (una colección con dos elementos).

Por este motivo, para admitir argumentos arbitrarios, creemos que se necesita una sintaxis más obvia para delimitar más claramente estos valores. En este espacio también se presentan otras preocupaciones de diseño. En ningún orden concreto, estos son:

  1. Que la solución no sea ambigua y provoque interrupciones en el código que es probable que los usuarios usen con expresiones de colección en la actualidad. Por ejemplo:

    List<Widget> c = [new(...), w1, w2, w3];
    

    Esto es legal en la actualidad, con la new(...) expresión siendo una "creación implícita de objetos" que crea un nuevo widget. No podemos reasignar esto para pasar argumentos al List<>constructor de , ya que ciertamente interrumpiría el código existente.

  2. Que la sintaxis no se extiende a fuera de la [...] construcción. Por ejemplo:

    HashSet<string> s = [...] with ...;
    

    Estas sintaxis se pueden interpretar para significar que la colección se crea primero y, a continuación, se vuelve a crear en una forma diferente, lo que implica varias transformaciones de los datos y costos más altos potencialmente no deseados (incluso si no se emite eso).

  3. Esto new como una palabra clave potencial para usar en absoluto en este espacio es poco confuso. Ambos porque [...]ya indican que se crea un nuevo objeto y, debido a que las traducciones de la expresión de colección pueden pasar por API que no son constructoras (por ejemplo, el patrón de método Create ).

  4. Que la solución no sea excesivamente detallada. Una propuesta de valor principal de las expresiones de colección es brevedad. Por lo tanto, si el formulario agrega una gran cantidad de scaffolding sintáctico, se sentirá como un paso hacia atrás y se reduce la propuesta de valor de usar expresiones de colección, en lugar de llamar a las API existentes para realizar la colección.

Tenga en cuenta que una sintaxis como new([...], ...) se ejecuta afoul de "2" y "3" anteriores. Esto hace que aparezca como si llamamos a un constructor (cuando no seamos) e implica que una expresión de colección creada se pasa a ese constructor, lo que definitivamente no es.

En función de todo lo anterior, se han llegado algunas opciones que se sienten para resolver las necesidades de pasar argumentos, sin salir de los límites de los objetivos de las expresiones de colección.

[with(...arguments...)] Diseño

Syntax:

collection_element
   : expression_element
   | spread_element
+  | with_element
   ;

+with_element
+  : 'with' argument_list
+  ;

Hay una ambigüedad sintáctica introducida inmediatamente con esta producción gramatical. De forma similar a la ambigüedad entre spread_element y expression_element (se explica aquí, hay una ambigüedad sintáctica inmediata entre with_element y expression_element. En concreto with(<arguments>) , es exactamente el cuerpo de producción para with_elementy también es accesible a través de expression_element -> expression -> ... -> invocation_expression. Hay una regla general sencilla para collection_elements. En concreto, si el elemento se inicia léxicamente con la secuencia with( de tokens, siempre se trata como .with_element

Esto es beneficioso de dos maneras. En primer lugar, una implementación del compilador solo necesita examinar los tokens siguientes inmediatamente que ve para determinar qué tipo de elemento se va a analizar. En segundo lugar, en consecuencia, un usuario puede comprender trivialmente qué tipo de elemento tiene sin tener que analizar mentalmente lo que sigue para ver si debería pensarlo como un with_element o expression_element.

Examples

Ejemplos de cómo se vería esto:

// With an existing type:

// Initialize to twice the capacity since we'll have to add
// more values later.
List<string> names = [with(capacity: values.Count * 2), .. values];

Estas formas parecen "leer" razonablemente bien. En todos esos casos, el código es "crear una expresión de colección, "con" los argumentos siguientes para pasar para controlar la instancia final y, a continuación, los elementos subsiguientes usados para rellenarla. Por ejemplo, la primera línea "crea una lista de cadenas "con" una capacidad de dos veces el recuento de los valores a punto de distribuirse en él".

Importantemente, este código tiene poca posibilidad de pasarse por alto como con formularios como, por ejemplo, [arg; element], al tiempo que agrega una detalle mínima, con una gran cantidad de flexibilidad para pasar los argumentos deseados.

Técnicamente sería un cambio importante, ya que with(...)podría haber sido una llamada a un método existente denominado with. Sin embargo, a diferencia new(...) de lo que es una manera conocida y recomendada de crear valores con tipo implícito, with(...) es mucho menos probable que un nombre de método ejecute afoul de nomenclatura de .Net para los métodos. En el improbable caso de que un usuario tuviera este tipo de método, sin duda podría seguir llamando al método existente mediante @with(...).

Traduciríamos este elemento de la with(...) siguiente manera:

List<string> names = [with(/*capacity*/10), ...]; // translates to:

// argument_list *becomes* the argument list for the
// constructor call. 
__result = new List<string>(10); // followed by normal initialization

// or

IList<string> names2 = [with(capacity: 20), ...]; // translates to:

__result = new List<string>(20);

En otras palabras, los argumentos argument_list se pasarían al constructor adecuado si llamamos a un constructor o al "método create" adecuado si llamamos a este método. También se permitiría proporcionar un único argumento heredado de los tipos de comparador BCL al crear una instancia de uno de los tipos de interfaz de diccionario de destino para controlar su comportamiento.

Conversions

La sección conversiones de las expresiones de colección se actualiza de la siguiente manera:

> A struct or class type that implements System.Collections.IEnumerable where:

-  * The type has an applicable constructor that can be invoked with no arguments, and the constructor is accessible at the location of the collection expression.
+  a. the collection expression has no `with_element` and the type has an applicable constructor
+     that can be invoked with no arguments, accessible at the location of the collection expression. or
+  b. the collection expression has a `with_element` and the type has at least one constructor
+     accessible at the location of the collection expression. 

Tenga en cuenta que los argumentos reales dentro argument_list del de with_element no afectan si la conversión existe o no. Sólo la presencia o ausencia del with_element propio. La intuición aquí es simplemente que si la expresión de colección se escribe sin una (como [x, y, z]) tendría que ser capaz de llamar al constructor sin argumentos. Aunque si lo tiene [with(...), x, y, z] , podría llamar al constructor adecuado. Esto también significa que los tipos que no se pueden invocar con un constructor sin argumentos se pueden usar con una expresión de colección, pero solo si esa expresión de colección que contiene un with_element.

A continuación se determina la determinación real de cómo afectará una with_element construcción.

Construcción

La construcción se actualiza de la siguiente manera.

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

Si se incluye collection_arguments y no es el primer elemento de la expresión de colección, se notifica un error en tiempo de compilación.

Si la lista de argumentos contiene valores con tipo dinámico , se notifica un error en tiempo de compilación (LDM-2025-01-22).

Constructores

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 y el tipo de destino no es un tipo de parámetro genérico, entonces:

  • La resolución de sobrecarga se usa para determinar el mejor constructor de instancia de los candidatos.
  • El conjunto de constructores candidatos es todos los constructores de instancia accesibles declarados en el tipo de destino que son aplicables con respecto a la lista de argumentos tal como se define en el miembro de función aplicable.
  • Si se encuentra un mejor constructor de instancia, se invoca al constructor con la lista de argumentos.
    • Si el constructor tiene un params parámetro, la invocación puede estar en forma expandida.
  • De lo contrario, se notifica un error de enlace.
// List<T> candidates:
//   List<T>()
//   List<T>(IEnumerable<T> collection)
//   List<T>(int capacity)
List<int> l;
l = [with(capacity: 3), 1, 2]; // new List<int>(capacity: 3)
l = [with([1, 2]), 3];         // new List<int>(IEnumerable<int> collection)
l = [with(default)];           // error: ambiguous constructor

Métodos CollectionBuilderAttribute

Si el tipo de destino es un tipo con un método create, haga lo siguiente:

  • La resolución de sobrecarga se usa para determinar el mejor método de creación de los candidatos.
  • Para cada método create para el tipo de destino, definimos un método de proyección con una firma idéntica al método create, pero sin el último parámetro.
  • El conjunto de métodos de proyección candidata es los métodos de proyección aplicables con respecto a la lista de argumentos tal como se define en el miembro de función aplicable.
  • Si se encuentra un mejor método de proyección, se invoca el método create correspondiente con la lista de argumentos anexada con un ReadOnlySpan<T> elemento que contiene los elementos .
  • De lo contrario, se notifica un error de enlace.
[CollectionBuilder(typeof(MyBuilder), "Create")]
class MyCollection<T> { ... }

class MyBuilder
{
    public static MyCollection<T> Create<T>(ReadOnlySpan<T> elements);
    public static MyCollection<T> Create<T>(IEqualityComparer<T> comparer, ReadOnlySpan<T> elements);
}
MyCollection<string> c1 = [with(GetComparer()), "1", "2"];
// IEqualityComparer<string> _tmp1 = GetComparer();
// ReadOnlySpan<string> _tmp2 = ["1", "2"];
// c1 = MyBuilder.Create<string>(_tmp1, _tmp2);

MyCollection<string> c2 = [with(), "1", "2"];
// ReadOnlySpan<string> _tmp3 = ["1", "2"];
// c2 = MyBuilder.Create<string>(_tmp3);

CollectionBuilderAttribute: Crear métodos

Para una expresión de colección donde la definición de tipo de destino tiene un [CollectionBuilder] atributo, los métodos create son los siguientes, actualizados a partir de expresiones de colección: create métodos.

Un [CollectionBuilder(...)] 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.

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 último 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.

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.

Las principales diferencias del algoritmo anterior son:

  • Es posible que los métodos de creación tengan parámetros adicionales antes del ReadOnlySpan<E> parámetro .
  • Se admiten varios métodos de creación.

Tipo de destino de interfaz

Si el tipo de destino es un tipo de interfaz, haga lo siguiente:

  • La resolución de sobrecarga se usa para determinar la mejor firma del método candidato.

  • El conjunto de firmas candidatas es las firmas siguientes para la interfaz de destino que son aplicables con respecto a la lista de argumentos tal como se define en el miembro de función aplicable.

    Interfaces Firmas candidatas
    IEnumerable<E>
    IReadOnlyCollection<E>
    IReadOnlyList<E>
    () (sin parámetros)
    ICollection<E>
    IList<E>
    List<E>()
    List<E>(int)

Si se encuentra una mejor firma de método, la semántica es la siguiente:

  • La firma candidata para IEnumerable<E>, IReadOnlyCollection<E> y es simplemente () y IReadOnlyList<E> tiene el mismo significado que no tener el with() elemento en absoluto.
  • Las firmas candidatas para IList<T> y ICollection<T> son las firmas de List<T>() los constructores y List<T>(int) . Al construir el valor (vea Traducción de interfaz mutable), se invocará el constructor correspondiente List<T> .
  • De lo contrario, se notifica un error de enlace.

tipo de destino Dictionary-Interface

Esto se especifica aquí como parte de la característica definida en https://github.com/dotnet/csharplang/blob/main/proposals/dictionary-expressions.md.

La lista anterior se aumenta para tener los siguientes elementos:

Interfaces Firmas candidatas
IReadOnlyDictionary<K, V> () (sin parámetros)
(IEqualityComparer<K>? comparer)
IDictionary<K, V> Dictionary<K, V>()
Dictionary<K, V>(int)
Dictionary<K, V>(IEqualityComparer<K>)
Dictionary<K, V>(int, IEqualityComparer<K>)

Si se encuentra una mejor firma de método, la semántica es la siguiente:

  • Las firmas candidatas para IReadOnlyDictionary<K, V> son () (que tiene el mismo significado que no tener el with() elemento en absoluto) y (IEqualityComparer<K>). Este comparador se usará para aplicar un hash adecuado y comparar las claves en el diccionario de destino que el compilador elige crear (consulte Traducción de interfaz no mutable).
  • Las firmas candidatas para IDictionary<T> son las firmas de Dictionary<K, V>()los constructores , Dictionary<K, V>(int)Dictionary<K, V>(IEqualityComparer<K>) y Dictionary<K, V>(int, IEqualityComparer<K>). Al construir el valor (vea Traducción de interfaz mutable), se invocará el constructor correspondiente Dictionary<K, V> .
  • De lo contrario, se notifica un error de enlace.
IDictionary<string, int> d;
IReadOnlyDictionary<string, int> r;

d = [with(StringComparer.Ordinal)]; // new Dictionary<string, int>(StringComparer.Ordinal)
r = [with(StringComparer.Ordinal)]; // new $PrivateImpl<string, int>(StringComparer.Ordinal)

d = [with(capacity: 2)]; // new Dictionary<string, int>(capacity: 2)
r = [with(capacity: 2)]; // error: 'capacity' parameter not recognized
d = [with()];            // Legal: empty arguments supported for interfaces

Otros tipos de destino

Si el tipo de destino es cualquier otro tipo, se notifica un error de enlace para la lista de argumentos, incluso si está vacío.

Span<int> a = [with(), 1, 2, 3]; // error: arguments not supported
Span<int> b = [with([1, 2]), 3]; // error: arguments not supported

int[] a = [with(), 1, 2, 3]; // error: arguments not supported
int[] b = [with(length: 1), 3]; // error: arguments not supported

Seguridad de referencias

Ajustamos las reglas collection-expressions.md#ref-safety para tener en cuenta el with() elemento.

Consulte también §16.4.15 Restricción de contexto seguro.

Creación de métodos

Esta sección se aplica a las expresiones de colección cuyo tipo de destino cumple las restricciones definidas en los métodos CollectionBuilderAttribute.

El contexto seguro se determina modificando una cláusula de collection-expressions.md#ref-safety (cambios en negrita):

  • 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 los argumentos son los with() argumentos de elemento seguidos por la expresión de colección como argumento para el último parámetro (el ReadOnlySpan<E> parámetro).

Los argumentos del método deben coincidir con la restricción se aplica a la expresión de colección. De forma similar a la determinación de contexto seguro anterior, los argumentos del método deben coincidir con la restricción se aplican tratando la expresión de colección como una invocación del método create, donde los argumentos son los with() argumentos de elemento seguidos por la expresión de colección como argumento para el último parámetro.

Llamadas de constructor

Esta sección se aplica a las expresiones de colección cuyo tipo de destino cumple las restricciones definidas en Constructores.

Para una expresión de colección de un tipo de estructura ref de la siguiente forma:
[with(a₁, a₂, ..., aₙ), e₁, e₂, ..., eₙ]

El contexto seguro de la expresión de colección es el más estrecho de los contextos seguros de las expresiones siguientes:

  • Expresión de creación new C(a₁, a₂, ..., aₙ)de objetos , donde C es el tipo de destino
  • Las expresiones e₁, e₂, ..., eₙ de elemento (ya sea las propias expresiones o el valor de propagación en el caso de un elemento de propagación).

Los argumentos del método deben coincidir con la restricción se aplica a la expresión de colección. La restricción se aplica tratando la expresión de colección como una creación de objetos del formulario new C(a₁, a₂, ..., aₙ) { e₁, e₂, ..., eₙ } por struct-improvements.md#rules-for-object-initializers.

  • Los elementos de expresión se tratan como si fueran inicializadores de elementos de colección.
  • Los elementos distribuidos se tratan de forma similar, suponiendo temporalmente que C tiene un Add(SpreadType spread) método, donde SpreadType es el tipo del valor de propagación.

Preguntas respondidas

Argumentos de dynamic

¿Se deben permitir argumentos con dynamic tipo? Esto podría requerir el uso del enlazador en tiempo de ejecución para la resolución de sobrecargas, lo que dificultaría limitar el conjunto de candidatos, por ejemplo, para los casos del generador de colecciones.

Resolución: No permitido. LDM-2025-01-22

with() cambio importante

El elemento propuesto with() es un cambio importante.

object x, y, z = ...;
object[] items = [with(x, y), z]; // C#13: ok; C#14: error args not supported for object[]

object with(object x, object y) { ... }

Confirme que el cambio importante es aceptable y si el cambio importante debe estar asociado a la versión del idioma.

Resolución: Mantenga el comportamiento anterior (sin cambios importantes) al compilar con la versión de lenguaje anterior. LDM-2025-03-17

¿Deben afectar los argumentos a la conversión de expresiones de colección?

¿Deben afectar los argumentos de colección y los métodos aplicables a la convertibilidad de la expresión de colección?

Print([with(comparer: null), 1, 2, 3]); // ambiguous or Print<int>(HashSet<int>)?

static void Print<T>(List<T> list) { ... }
static void Print<T>(HashSet<T> set) { ... }

Si los argumentos afectan a la convertibilidad en función de los métodos aplicables, es probable que los argumentos también afecten a la inferencia de tipos.

Print([with(comparer: StringComparer.Ordinal)]); // Print<string>(HashSet<string>)?

Como referencia, los casos similares con el tipo de destino producen new() errores.

Print<int>(new(comparer: null));              // error: ambiguous
Print(new(comparer: StringComparer.Ordinal)); // error: type arguments cannot be inferred

Resolución: Los argumentos de colección deben omitirse en conversiones y inferencia de tipos. LDM-2025-03-17

Orden de parámetros del método del generador de colecciones

En el caso de los métodos del generador de colecciones , ¿el parámetro span debe estar antes o después de cualquier parámetro para los argumentos de colección?

En primer lugar, los elementos permitirían declarar los argumentos como opcionales.

class MySetBuilder
{
    public static MySet<T> Create<T>(ReadOnlySpan<T> items, IEqualityComparer<T> comparer = null) { ... }
}

Los argumentos primero permitirían que el intervalo sea un params parámetro, para admitir la llamada directamente en formato expandido.

var s = MySetBuilder.Create(StringComparer.Ordinal, x, y, z);

class MySetBuilder
{
    public static MySet<T> Create<T>(IEqualityComparer<T> comparer, params ReadOnlySpan<T> items) { ... }
}

Resolución: El parámetro span de los elementos debe ser el último parámetro. LDM-2025-03-12

Argumentos con versión de idioma anterior

¿Se notifica un error al with() compilar con una versión de idioma anterior o se with enlaza a otro símbolo en el ámbito?

Resolución: No hay ningún cambio importante para with dentro de una expresión de colección al compilar con versiones de lenguaje anteriores. LDM-2025-03-17

Tipos de destino en los que se requieren argumentos

¿Se deben admitir conversiones de expresiones de colección en tipos de destino en los que se deben proporcionar argumentos porque todos los constructores o métodos de fábrica requieren al menos un argumento?

Estos tipos se pueden usar con expresiones de colección que incluyen argumentos explícitos with() , pero los tipos no se pueden usar para params los parámetros.

Por ejemplo, considere el siguiente tipo construido a partir de un método de fábrica:

MyCollection<object> c;
c = [];                  // error: no arguments
c = [with(capacity: 1)]; // ok

[CollectionBuilder(typeof(MyBuilder), "Create")]
class MyCollection<T> : IEnumerable<T> { ... }

class MyBuilder
{
    public static MyCollection<T> Create<T>(ReadOnlySpan<T> items, int capacity) { ... }
}

La misma pregunta se aplica para cuando se llama al constructor directamente como en el ejemplo siguiente.

Sin embargo, para los tipos de destino a los que se llama directamente al constructor, la conversión de expresiones de colección requiere actualmente un constructor al que no se puede llamar sin argumentos, pero los argumentos de la colección se omiten al determinar la convertibilidad.

c = [];                  // error: no arguments
c = [with(capacity: 1)]; // error: no constructor callable with no arguments?

class MyCollection<T> : IEnumerable<T>
{
    public MyCollection(int capacity) { ... }
    public void Add(T t) { ... }
    // ...
}

Resolución: Admite conversiones a tipos de destino en los que todos los constructores o métodos de fábrica requieren argumentos y requieren with() para la conversión. LDM-2025-03-05

__arglist

¿Se debe __arglist admitir en with() elementos?

class MyCollection : IEnumerable
{
    public MyCollection(__arglist) { ... }
    public void Add(object o) { }
}

MyCollection c;
c = [with(__arglist())];    // ok
c = [with(__arglist(x, y)]; // ok

Resolución: No se admite __arglist en argumentos de colección a menos que sea gratuito. LDM-2025-03-05

Argumentos para tipos de interfaz

¿Se deben admitir argumentos para los tipos de destino de interfaz?

ICollection<int> c = [with(capacity: 4)];
IReadOnlyDictionary<string, int> d = [with(comparer: StringComparer.Ordinal), ..values];
Si es así, ¿qué firmas de método se usan al enlazar los argumentos?

Para los tipos de interfaz mutables , las opciones son:

  1. Use los constructores accesibles del tipo conocido necesario para la creación de instancias: List<T> o Dictionary<K, V>.
  2. Use firmas independientes del tipo específico, por ejemplo, usando new() y new(int capacity) para ICollection<T> y IList<T> (consulte Construcción de posibles firmas para cada interfaz).

El uso de los constructores accesibles desde un tipo conocido tiene las siguientes implicaciones:

  • Los nombres de parámetro, optional-ness, params, se toman directamente de los parámetros.
  • Se incluyen todos los constructores accesibles, aunque esto no sea útil para las expresiones de colección, como List(IEnumerable<T>) las que permitirían IList<int> list = [with(1, 2, 3)];.
  • El conjunto de constructores puede depender de la versión de BCL.

Recomendación: use los constructores accesibles de los tipos conocidos. Hemos garantizado que usaríamos estos tipos, por lo que esto simplemente "cae" y es la ruta más clara y sencilla para construir estos valores.

En el caso de los tipos de interfaz no mutables , las opciones son similares:

  1. No hacer nada. Éste
  2. Use firmas independientes del tipo específico, aunque el único escenario puede ser new(IEqualityComparer<K> comparer) para IReadOnlyDictionary<K, V> C#14.

El uso de constructores accesibles de algún tipo conocido (la estrategia para tipos mutable-interface-types) no es viable, ya que no hay ninguna relación con ningún tipo existente determinado y el tipo final que podemos usar o sintetizar. Por lo tanto, tendría que haber nuevos requisitos extraños que el compilador pueda asignar cualquier constructor existente de dicho tipo (incluso a medida que evoluciona) a la instancia no mutable que realmente genera.

Recomendación: use firmas independientes de un tipo específico. Y, para C# 14, solo se admite new(IEqualityComparer<K> comparer) como IReadOnlyDictionary<K, V> esa es la única interfaz no mutable en la que creemos que es fundamental para la facilidad de uso o semántica para permitir a los usuarios proporcionar esto. Las versiones futuras de C# pueden considerar la posibilidad de expandir este conjunto en función de las justificaciones sólidas proporcionadas.

Resolución:https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-04-23.md

Los argumentos se admiten para los tipos de destino de interfaz. Para las interfaces mutables y no mutables, se seleccionará el conjunto de argumentos.

La lista esperada (que todavía debe ser ratificada por LDM) es tipo de destino interface

Listas de argumentos vacías

¿Deberíamos permitir listas de argumentos vacías para algunos o todos los tipos de destino?

Un vacío with() sería equivalente a ningún with(). Podría proporcionar cierta coherencia con casos no vacíos, pero no agregaría ninguna nueva funcionalidad.

El significado de un 'with()' vacío podría ser más claro para algunos tipos de destino que otros: - Para los tipos en los que se usan **constructores**, llame al constructor aplicable sin argumentos. - Para los tipos con **'CollectionBuilderAttribute'**, llame al método de fábrica aplicable solo con elementos. - Para **tipos de interfaz**, construya el tipo conocido o definido por la implementación sin argumentos. - Sin embargo, para **matrices** y **spans**, donde no se admiten argumentos de colección, 'with()' puede resultar confuso.
List<int>           l = [with()]; // ok? new List<int>()
ImmutableArray<int> m = [with()]; // ok? ImmutableArray.Create<int>()

IList<int>       i = [with()]; // ok? new List<int>() or equivalent
IEnumerable<int> e = [with()]; // ok?

int[]     a = [with()]; // ok?
Span<int> s = [with()]; // ok?

Resolución:https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-05-12.md#empty-argument-lists

Se permitirá with() para tipos de constructor y tipos de generador que se pueden llamar sin argumentos en absoluto, y agregaremos firmas de constructor vacías para los tipos de interfaz (mutable y readonly). Las matrices y los intervalos no permitirán con(), ya que no hay firmas que se ajusten a ellas.

Preguntas abiertas

Finalización de una preocupación abierta de https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-03-17.md#conclusion

with(...) es un cambio importante en el idioma con [with(...)]. Antes de esta característica, significa una expresión de colección con un elemento, que es el resultado de llamar a with-invocation-expression. Después de esta característica, es una colección, que tiene argumentos pasados a ella.

¿Deseamos que esta interrupción se produzca solo cuando un usuario elige una versión de idioma específica (como C#-14/15?). En otras palabras, si se encuentran en una versión de langversion anterior, obtienen la lógica de análisis anterior, pero en la versión más reciente obtienen la lógica de análisis más reciente. ¿O siempre queremos que tenga la lógica de análisis más reciente, incluso en una langversion anterior?

Tenemos arte previo para ambas estrategias. required, por ejemplo, siempre se analiza con la nueva lógica, independientemente de langversion. Mientras que, record/field y otros changan su lógica de análisis en función de la versión del lenguaje.

Por último, esto tiene superposición e impacto con Dictionary Expressions, que presenta la key:value sintaxis de los elementos KVP. Queremos establecer el comportamiento que queremos para cualquier versión de lang, y por [with(...)] su cuenta, y cosas como [with(...) : expr] o [expr : with(...)].