Collection expressions - C# feature specifications (original) (raw)

Note

This article is a feature specification. The specification serves as the design document for the feature. It includes proposed specification changes, along with information needed during the design and development of the feature. These articles are published until the proposed spec changes are finalized and incorporated in the current ECMA specification.

There may be some discrepancies between the feature specification and the completed implementation. Those differences are captured in the pertinent language design meeting (LDM) notes.

You can learn more about the process for adopting feature speclets into the C# language standard in the article on the specifications.

Champion issue: https://github.com/dotnet/csharplang/issues/8652

Summary

Collection expressions introduce a new terse syntax, [e1, e2, e3, etc], to create common collection values. Inlining other collections into these values is possible using a spread element ..e like so: [e1, ..c2, e2, ..c2].

Several collection-like types can be created without requiring external BCL support. These types are:

Further support is present for collection-like types not covered under the above through a new attribute and API pattern that can be adopted directly on the type itself.

Motivation

An inclusive solution is needed for C#. It should meet the vast majority of casse for customers in terms of the collection-like types and values they already have. It should also feel natural in the language and mirror the work done in pattern matching.

This leads to a natural conclusion that the syntax should be like [e1, e2, e3, e-etc] or [e1, ..c2, e2], which correspond to the pattern equivalents of [p1, p2, p3, p-etc] and [p1, ..p2, p3].

Detailed design

The following grammar productions are added:

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
  ;

Collection literals are target-typed.

Spec clarifications

Conversions

A collection expression conversion allows a collection expression to be converted to a type.

An implicit collection expression conversion exists from a collection expression to the following types:

The implicit conversion exists if the type has an element type T where for each element Eᵢ in the collection expression:

There is no collection expression conversion from a collection expression to a multi dimensional array type.

Types for which there is an implicit collection expression conversion from a collection expression are the valid target types for that collection expression.

The following additional implicit conversions exist from a collection expression:

Create methods

A create method is indicated with a [CollectionBuilder(...)] attribute on the collection type. The attribute specifies the builder type and method name of a method to be invoked to construct an instance of the collection type.

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

The attribute can be applied to a class, struct, ref struct, or interface. The attribute is not inherited although the attribute can be applied to a base class or an abstract class.

The builder type must be a non-generic class or struct.

First, the set of applicable create methods CM is determined.
It consists of methods that meet the following requirements:

Methods declared on base types or interfaces are ignored and not part of the CM set.

If the CM set is empty, then the collection type doesn't have an element type and doesn't have a create method. None of the following steps apply.

If only one method among those in the CM set has an identity conversion from E to the element type of the collection type, that is the create method for the collection type. Otherwise, the collection type doesn't have a create method.

An error is reported if the [CollectionBuilder] attribute does not refer to an invokable method with the expected signature.

For a collection expression with a target type C<S0, S1, …> where the type declaration C<T0, T1, …> has an associated builder method B.M<U0, U1, …>(), the generic type arguments from the target type are applied in order — and from outermost containing type to innermost — to the builder method.

The span parameter for the create method can be explicitly marked scoped or [UnscopedRef]. If the parameter is implicitly or explicitly scoped, the compiler may allocate the storage for the span on the stack rather than the heap.

For example, a possible create method for ImmutableArray<T>:

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

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

With the create method above, ImmutableArray<int> ia = [1, 2, 3]; could be emitted as:

[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

The elements of a collection expression are evaluated in order, left to right. Each element is evaluated exactly once, and any further references to the elements refer to the results of this initial evaluation.

A spread element may be iterated before or after the subsequent elements in the collection expression are evaluated.

An unhandled exception thrown from any of the methods used during construction will be uncaught and will prevent further steps in the construction.

Length, Count, and GetEnumerator are assumed to have no side effects.


If the target type is a struct or class type that implements System.Collections.IEnumerable, and the target type does not have a create method, the construction of the collection instance is as follows:


If the target type is an array, a span, a type with a create method, or an interface, the construction of the collection instance is as follows:


_Note:_The compiler may delay adding elements to the collection — or delay iterating through spread elements — until after evaluating subsequent elements. (When subsequent spread elements have countable properties that would allow calculating the expected length of the collection before allocating the collection.) Conversely, the compiler may eagerly add elements to the collection — and eagerly iterate through spread elements — when there is no advantage to delaying.

Consider the following collection expression:

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

If spread elements b and c are countable, the compiler could delay adding items from a and b until after c is evaluated, to allow allocating the resulting array at the expected length. After that, the compiler could eagerly add items from c, before evaluating 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;

Empty collection literal

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

Here, if b is false, it is not required that any value actually be constructed for the empty collection expression since it would immediately be spread into zero values in the final literal.

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

Ref safety

See safe context constraint for definitions of the safe-context values: declaration-block, function-member, and caller-context.

The safe-context of a collection expression is:

A collection expression with a safe-context of declaration-block cannot escape the enclosing scope, and the compiler may store the collection on the stack rather than the heap.

To allow a collection expression for a ref struct type to escape the declaration-block, it may be necessary to cast the expression to another type.

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

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

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

Type inference

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;

The type inference rules are updated as follows.

The existing rules for the first phase are extracted to a new input type inference section, and a rule is added to input type inference and output type inference for collection expression expressions.

11.6.3.2 The first phase

For each of the method arguments Eᵢ:

An input type inference is made from an expression E to a type T in the following way:

11.6.3.7 Output type inferences

An output type inference is made from an expression E to a type T in the following way:

Extension methods

No changes to extension method invocation rules.

12.8.10.3 Extension method invocations

An extension method Cᵢ.Mₑ is eligible if:

A collection expression does not have a natural type so the existing conversions from type are not applicable. As a result, a collection expression cannot be used directly as the first parameter for an extension method invocation.

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

Overload resolution

Better conversion from expression is updated to prefer certain target types in collection expression conversions.

In the updated rules:

Given an implicit conversion C₁ that converts from an expression E to a type T₁, and an implicit conversion C₂ that converts from an expression E to a type T₂, C₁ is a better conversion than C₂ if one of the following holds:

Examples of differences with overload resolution between array initializers and collection expressions:

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

Span types

The span types ReadOnlySpan<T> and Span<T> are both constructible collection types. Support for them follows the design for params Span. Specifically, constructing either of those spans will result in an array T[] created on the stack if the params array is within limits (if any) set by the compiler. Otherwise the array will be allocated on the heap.

If the compiler chooses to allocate on the stack, it is not required to translate a literal directly to a stackalloc at that specific point. For example, given:

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

The compiler is allowed to translate that using stackalloc as long as the Span meaning stays the same and span-safety is maintained. For example, it can translate the above to:

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
}

The compiler can also use inline arrays, if available, when choosing to allocate on the stack. Note that in C# 12, inline arrays can't be initialized with a collection expression. That feature is an open proposal.

If the compiler decides to allocate on the heap, the translation for Span<T> is simply:

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

Collection literal translation

A collection expression has a known length if the compile-time type of each spread element in the collection expression is countable.

Interface translation

Non-mutable interface translation

Given a target type which does not contain mutating members, namely IEnumerable<T>, IReadOnlyCollection<T>, and IReadOnlyList<T>, a compliant implementation is required to produce a value that implements that interface. If a type is synthesized, it is recommended the synthesized type implements all these interfaces, as well as ICollection<T> and IList<T>, regardless of which interface type was targeted. This ensures maximal compatibility with existing libraries, including those that introspect the interfaces implemented by a value in order to light up performance optimizations.

In addition, the value must implement the nongeneric ICollection and IList interfaces. This enables collection expressions to support dynamic introspection in scenarios such as data binding.

A compliant implementation is free to:

  1. Use an existing type that implements the required interfaces.
  2. Synthesize a type that implements the required interfaces.

In either case, the type used is allowed to implement a larger set of interfaces than those strictly required.

Synthesized types are free to employ any strategy they want to implement the required interfaces properly. For example, a synthesized type might inline the elements directly within itself, avoiding the need for additional internal collection allocations. A synthesized type could also not use any storage whatsoever, opting to compute the values directly. For example, returning index + 1 for [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].

  1. The value must return true when queried for ICollection<T>.IsReadOnly (if implemented) and nongeneric IList.IsReadOnly and IList.IsFixedSize. This ensures consumers can appropriately tell that the collection is non-mutable, despite implementing the mutable views.
  2. The value must throw on any call to a mutation method (like IList<T>.Add). This ensures safety, preventing a non-mutable collection from being accidentally mutated.

Mutable interface translation

Given target type that contains mutating members, namely ICollection<T> or IList<T>:

  1. The value must be an instance of List<T>.

Known length translation

Having a known length allows for efficient construction of a result with the potential for no copying of data and no unnecessary slack space in a result.

Not having a known length does not prevent any result from being created. However, it may result in extra CPU and memory costs producing the data, then moving to the final destination.

int __len = count_of_expression_elements +  
            __s1.Count;  
            ...  
            __s_n.Count;  

Unknown length translation

Unsupported scenarios

While collection literals can be used for many scenarios, there are a few that they are not capable of replacing. These include:

Syntax ambiguities

Drawbacks

This does introduce warts into the language. For example, the following are both legal and (fortunately) mean the exact same thing:

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

However, given the breadth and consistency brought by the new literal syntax, we should consider recommending that people move to the new form. IDE suggestions and fixes could help in that regard.

Alternatives

Resolved questions

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

However, this is unfortunate due to the need to force allocations of temporary storage. We could potentially be more efficient if we controlled how this was emitted.

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

Resolution: Yes, a literal can be target-typed to any interface type I<T> that List<T> implements. For example, IEnumerable<long>. This is the same as target-typing to List<long> and then assigning that result to the specified interface type. The following text exists to record the original discussion of this topic.
The open question here is determining what underlying type to actually create. One option is to look at the proposal for params IEnumerable. There, we would generate an array to pass the values along, similar to what happens with params T[].

Unresolved questions

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;
}
Span<int> span = [a, ..b ? [c] : [d, e], f];  

Note: this may commonly come up in the following form to allow conditional inclusion of some set of elements, or nothing if the condition is false:

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

In order to evaluate this full literal, we need to evaluate the element expressions within. That means being able to evaluate b ? [c] : [d, e]. However, absent a target type to evaluate this expression in the context of, and absent any sort of natural type, this would we would be unable to determine what to do with either [c] or [d, e] here.
To resolve this, we could say that when evaluating a literal's spread_element expression, there was an implicit target type equivalent to the target type of the literal itself. So, in the above, that would be rewritten as:

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;  

Specification of a constructible collection type utilizing a create method is sensitive to the context at which conversion is classified

An existence of the conversion in this case depends on the notion of an iteration typeof the collection type. If there is a create method that takes a ReadOnlySpan<T> where T is the iteration type, the conversion exists. Otherwise, it doesn't.

However, an iteration typeis sensitive to the context at which foreach is performed. For the same collection type it can be different based on what extension methods are in scope, and it can also be undefined.

That feels fine for the purpose of foreach when the type isn't designed to be foreach-able on itself. If it is, extension methods cannot change how the type is foreach-ed over, no matter what the context is.

However, that feels somewhat strange for a conversion to be context sensitive like that. Effectively the conversion is "unstable". A collection type explicitly designed to be constructible is allowed to leave out a definition of a very important detail - its iteration type. Leaving the type "unconvertible" on itself.

Here is an example:

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

Given the current design, if the type doesn't define iteration type itself, compiler is unable to reliably validate an application of a CollectionBuilder attribute. If we don't know the iteration type, we don't know what the signature of the create method should be. If the iteration type comes from context, there is no guarantee that the type is always going to be used in a similar context.

Params Collections feature is also affected by this. It feels strange to be unable to reliably predict element type of a params parameter at the declaration point. The current proposal also requires to ensure that the create method is at least as accessible as theparams collection type. It is impossible to perform this check in a reliable fashion, unless the _collection type_defines its iteration type itself.

Note, that we also have https://github.com/dotnet/roslyn/issues/69676 opened for compiler, which basically observes the same issue, but talks about it from the perspective of optimization.

Proposal

Require a type utilizing CollectionBuilder attribute to define its iteration type on itself. In other words this means, that the type should either implement IEnumarable/IEnumerable<T>, or it should have public GetEnumerator method with the right signature (this excludes any extension methods).

Also, right now create method is required to "be accessible where the collection expression is used". This is another point of context dependency based on accessibility. The purpose of this method is very similar to the purpose of a user-defined conversion method, and that one must be public. Therefore, we should consider requiring the create method to be public as well.

Conclusion

Approved with modifications LDM-2024-01-08

The notion of iteration type is not applied consistently throughout conversions

It looks like an assumption is made that T is necessary the iteration type of the struct or class type in this case. However, that assumption is incorrect. Which can lead to a very strange behavior. For example:

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

It looks like implementation assumes that the iteration type is object, but the specification leaves this fact unspecified, and simply doesn't require each element to convert to anything. In general, however, the iteration type is not necessary the object type. Which can be observed in the following example:

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

The notion of iteration type is fundamental to Params Collections feature. And this issue leads to a strange discrepancy between the two features. For Example:

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

It will probably be good to align one way or the other.

Proposal

Specify convertibility of struct or class type that implements System.Collections.Generic.IEnumerable<T> or System.Collections.IEnumerablein terms of iteration type and require an implicit conversion for each element Ei to the iteration type.

Conclusion

Approved LDM-2024-01-08

Should collection expression conversion require availability of a minimal set of APIs for construction?

A constructible collection type according to conversions can actually be not constructible, which is likely to lead to some unexpected overload resolution behavior. For example:

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

However, the 'C1.M1(string)' is not a candidate that can be used because:

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

Here is another example with a user-defined type and a stronger error that doesn't even mention a valid candidate:

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

It looks like the situation is very similar to what we used to have with method group to delegate conversions. I.e. there were scenarios where the conversion existed, but was erroneous. We decided to improve that by ensuring that, if conversion is erroneous, then it doesn't exist.

Note, that with "Params Collections" feature we will be running into a similar issue. It might be good to disallow usage of params modifier for not constructible collections. However in the current proposal that check is based onconversions section. Here is an example:

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

It looks like the issue was somewhat discussed previously, see https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions. At that time an argument was made that the rules, as specified right now, are consistent with how interpolated string handlers are specified. Here is a quote:

In particular, interpolated string handlers were originally specified this way, but we revised the specification after considering this issue.

While there is some similarity, there is also an important distinction worth considering. Here is a quote from https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion:

Type T is said to be an applicable_interpolated_string_handler_type if it is attributed with System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. There exists an implicit interpolated_string_handler_conversion to T from an interpolated_string_expression, or an additive_expression composed entirely of _interpolated_string_expression_s and using only + operators.

The target type must have a special attribute which is a strong indicator of author's intent for the type to be an interpolated string handler. It is fair to assume that presence of the attribute is not a coincidence. In contrast, the fact that a type is "enumerable", doesn't necessary mean that there was author's intent for the type to be constructible. A presence of a create method, however, which is indicated with a [CollectionBuilder(...)] attribute on the collection type, feels like a strong indicator of author's intent for the type to be constructible.

Proposal

For a struct or class type that implements System.Collections.IEnumerable and that does not have a create method conversions section should require presence of at least the following APIs:

For the purpose of Params Collectons feature, such types are valid params types when these APIs are declared public and are instance (vs. extension) methods.

Conclusion

Approved with modifications LDM-2024-01-10

Design meetings

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

Working group meetings

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

Upcoming agenda items

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

What happens with M([1, 2, 3])? We likely need to define 'betterness' for these conversions.

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

It seems natural that this should be something the inference algorithm can be made aware of. Once this is supported for the 'base' constructible collection type cases (T[], I<T>, Span<T> new T()), then it should also fall out of the Collect(constructible_type) case. For example:

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

Here, Immutable<T> is constructible through an init void Construct(T[] values) method. So the T[] values type would be used with inference against [1, 2, 3] leading to an inference of int for T.

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

But it would be nice to be able to do things like:

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

Can/should we take a break here?