- Proposed
- Prototype
- Implementation
- Specification
C# has support for params
parameters in parameter lists on methods, constructors, and indexers. The type of a params
parameter is restricted to single-dimensional array types.
Support for params IEnumerable<T>
has been suggested before, but a more general pattern starts to show up when constructing APIs that build immutable data structures. A good example is the System.Linq.Expressions
API where factory methods accept a params Expression[]
but need to create a defensive copy of the array to ensure immutability (using an internal TrueReadOnlyCollection<T>
type). This causes excessive allocations, even if the params
array was constructed by the compiler, which ensures only the callee can observe the reference to the array, so external mutation by the caller is not possible.
This proposal suggests a collection initialization builder pattern that is supported by params
parameters "arrays".
In the section on Method parameters, the grammar production for parameter_array is altered as follows:
parameter_array
: attributes? 'params' type identifier
;
where type was substituted for array_type.
The following paragraph is changed:
A parameter_array consists of an optional set of attributes (Attributes), a
params
modifier, an array_type, and an identifier. A parameter array declares a single parameter of the given array type with the given name. The array_type of a parameter array must be a single-dimensional array type (Array types). In a method invocation, a parameter array permits either a single argument of the given array type to be specified, or it permits zero or more arguments of the array element type to be specified. Parameter arrays are described further in Parameter arrays.
to
A parameter array type is either single-dimensional array type (Array types) or a type that has an associated parameter array builder type.
The parameter array builder type
B
is derived from a candidate parameter array typeT
by obtaining the type specified in the single argument on theSystem.Runtime.CompilerServices.CollectionBuilderAtribute
custom attribute applied toT
, ifT
is a non-generic type, or on the open generic type definition ofT
ifT
is a closed generic type. Base classes are not considered to locate this custom attribute.If
T
is a closed generic type withN
type arguments,B
should be an open generic type withN
type parameters. If this condition is not met,T
is not classified as a parameter array type. Otherwise, we redefineB
to be the closed generic instantiation using the type arguments ofT
for the remainder of this section.
B
is aclass
or astruct
and has the following public methods:{ public static I Create(int length); }where
I
is an intermediate type which may be different fromB
.I
is aclass
or astruct
and has the following public methods:{ public void Add(E element); public void R GetResult(); }The type
E
on theAdd
method is the inferred parameter array element type. If multiple overloads ofAdd
exist with 1 parameter,E
cannot be determined uniquely andT
is not classified as a parameter array type.Note that
E
can never be an open generic type becauseAdd
andCreate
are not allowed to have type parameters, andB
is either non-generic or a closed generic type using the type arguments ofT
.The return type
R
ofGetResult
should be implicitly convertible toT
(Implicit conversions).If any of the preceding requirements is not met,
T
is not classified as a parameter array type.A parameter_array consists of an optional set of attributes (Attributes), a
params
modifier, a type, and an identifier. A parameter array declares a single parameter of the given type with the given name. The type of a parameter array must be a parameter array type. In a method invocation, a parameter array permits either a single argument of the given array type to be specified, or it permits zero or more arguments of the array element type to be specified. Parameter arrays are described further in Parameter arrays.
Note that the section on Parameter arrays refers to the "element type of the parameter array" which is either:
- the element type of the array type, if the parameter array type is a single-dimensional array type, or,
- the inferred parameter array element type
E
otherwise.
The rules for Better function member need to be reviewed, because the set of applicable function members can grow because more function members with a parameter array can become applicable in their expanded form. Betterness between two expanded forms may have to be revisited.
Note that there is no immediate breaking change potential by introducing parameter array support for types other than single-dimensional arrays, because it's currently invalid to use the params
modifier on types other than single-dimensional arrays.
An example of a parameter array builder type for ImmutableArray<T>
is shown below:
[CollectionBuilder(typeof(ImmutableArrayBuilder<>))]
public struct ImmutableArray<T>
{
internal ImmutableArray(T[] items) { ... }
}
public struct ImmutableArrayBuilder<T>
{
private readonly T[] _array;
private int _index;
public ImmutableArrayBuilder<T>(int length)
{
_array = new T[length];
_index = 0;
}
public static ImmutableArrayBuilder<T> Create(int length) =>
new ImmutableArrayBuilder<T>(length);
public void Add(T element) => [_index++] = element;
public ImmutableArray<T> GetResult() => new ImmutableArray<T>(_array);
}
This enables a user to write the following method:
void F(int a, params ImmutableArray<int> array) { ... }
and call it like this:
F(x(), y(), z());
which results in the following code generation:
var t0 = x();
var t1 = y();
var t2 = z();
var t3 = ImmutableArrayBuilder<int>.Create(2);
t3.Add(t1);
t3.Add(t2);
F(t0, t3.GetResult());
where variables t0
to t3
are compiler-generated.
Note that the use of this feature works well with immutable collection types but is not exclusive to such types. Even mutable collection types such as lists could benefit from params
support with the added benefit of inferring the initial capacity at compile time. This helps to optimize code like this:
Initialize(new List<int> { 1, 2, 3, 4, 5 })
which does not specify an initial capacity, or code like this:
Initialize(new List<int>(4) { 1, 2, 3, 4, 5 })
where someone added an element and forgot to update the capacity. If a type such as List<T>
were to add support for a params
builder type, both issues can be avoided and the call-site syntax gets cleaner:
Initialize(1, 2, 3, 4, 5)
Should a change to collection initializer expressions be considered to support builder types? For example:
new List<int> { 1, 2, 3, 4, 5 }
could benefit from the builder approach because the capacity can be statically determined at compile time. An example with immutable arrays would become even more obvious:
new ImmutableArray<int> { 1, 2, 3, 4, 5 }
because there is no similar concise way of creating these without undesirable allocations, for example:
// a params int[] allocation, and a defensive copy
ImmutableArray.Create<int>(1, 2, 3, 4, 5)
// a builder allocation, a params int[] allocation, and a defensive copy
ImmutableArray.CreateBuilder<int>(5).AddRange(1, 2, 3, 4, 5).ToImmutable()
// a builder allocation and incredibly verbose
var t = ImmutableArray.CreateBuilder<int>(5);
t.Add(1);
t.Add(2);
t.Add(3);
t.Add(4);
t.Add(t);
var array = t.ToImmutable();
// NOT POSSIBLE because the nested Builder type has an internal constructor
// a builder allocation and lots of decoration
new ImmutableArray<T>.Builder(5) { 1, 2, 3, 4, 5 }.ToImmutable()
The last form is almost exactly what the proposal's code generation does, if one substitutes the Builder
constructor invocation for a Create
method invocation, unrolls the collection initializer to Add
methods, and replaces ToImmutable
by GetResult
.
There are some concerns to be addressed. First, adding a builder type to an existing type can cause subtle change of behavior. In the examples above, the second case of ImmutableArray<int>
was never possible due to the lack of an Add
method, so there's no issue there. For the first case of List<int>
, the behavior changes from invoking the default constructor on the type to emitting code against the associated builder type.
One could argue that library writers should ensure that adding a collection builder type to an existing type does not cause any change in observable behavior when used with a collection initializer. For the case of List<T>
this would be very achievable.
Second, collection initializers also support concise initialization syntax using Add
methods with more than 1 parameter.
new Dictionary<string, int> { { "a", 1 } }
Supporting this in combination with builder types would require revisting the definitions earlier in this proposal such that Add
has element types E_1
to E_N
as parameters, and usage in the context of params
assignment always picks the overload of Add
that has 1 parameter. We'd have to define what it means to have multiple overloads of Add
with 1 parameter. This combination would make the following valid:
[CollectionBuilder(typeof(DictionaryBuilder<,>))]
public class Dictionary<K, V>
{
...
}
public struct DictionaryBuilder<K, V>
{
private readonly Dictionary<K, V> _dictionary;
public DictionaryBuilder(int length)
{
_dictionary = new Dictionary<K, V>(length);
}
public static DictionaryBuilder<K, V> Create(int length) =>
new DictionaryBuilder<K, V>(length);
public void Add(KeyValuePair<K, V> element) =>
_dictionary.Add(element.Key, element.Value);
public void Add(K key, V value) =>
_dictionary.Add(key, value);
public Dictionary<K, V> GetResult() => _dictionary;
}
Collection initializer expressions would be able to bind to either Add
method overload:
new Dictionary<string, int> {
{ "a", 1 },
new KeyValuePair<string, int>("b", 2)
}
Assignment to a params
array would work equally well:
InitializeDictionary<string, int>(
new KeyValuePair<string, int>("a", 1),
new KeyValuePair<string, int>("b", 2)
)
but only binding to the Add
method with 1 parameter. Thinking about the proposal in this way, one could argue that params
is a shorthand inline array initializer and the previous example is equivalent to:
InitializeDictionary<string, int>(new Dictionary<string, int> {
new KeyValuePair<string, int>("a", 1),
new KeyValuePair<string, int>("b", 2)
})
just like
Sum(1, 2, 3)
is equivalent to:
Sum(new int[] { 1, 2, 3 })
and both examples are a means to convert from the expanded form to the normal form by introducing an object creation expression. This could be the more elegant way to formulate params
collection builder support in terms of a desugaring "lowering" step to a collection initializer expression.
Making the following work is likely a bridge too far:
InitializeDictionary<string, int>(
{ "a", 1 },
{ "b", 2 }
)
where collection initialization syntax can be used when binding to params
in the expanded form, supporting binding to Add
methods with more than 1 parameter. It's not at all clear how candidate members would be selected because these element initializers are not expressions and don't contribute any types. However, if one adds an Add
method with a tuple:
public void Add((K key, V value) element) =>
_dictionary.Add(element.key, element.value);
almost the same concise syntax would work:
InitializeDictionary<string, int>(
("a", 1),
("b", 2)
)
because the element type E
can be inferred as a (string, int)
tuple type. The question on having multiple overloads of Add
with 1 parameter still comes up though.
It'd also be interesting to consider target-typed new
with collection initializers and builders. Would the following work as an abbreviated target-typed collection initializer expression variant?
new
{
{ "a", 1 },
{ "b", 2 }
}
if the type of the new
expression is inferred to be Dictionary<string, int>
? If so, one could still write:
InitializeDictionary<string, int>(new {
{ "a", 1 },
{ "b", 2 }
})
which would not involve any params
expanded form binding, but the collection initializer expression may still bind to a builder.