0

Consider:

class MyList<T> {
  public MyList(params T[] ts) { /* add all items to internal list */ }
  public MyList(params IEnumerable<T>[] ets) { /* add all items from all enumerables to internal list */ }
}

And then

record Employee(string Name, int Age);

var l1 = new MyList<int>(1, 2, 3); // ok

IEnumerable<Employee> e = new List<Employee>() { new("John", 30), new("Eric", 30) };
var l2 = new MyList<Employee>(e, e); // ok

var l3 = new MyList<Employee>(
  new("John", 30),
  new("Eric", 30)
);

For l3 I get:

The call is ambiguous between the following methods or properties: MyList<T>.MyList(params T[]) and MyList<T>.MyList(params IEnumerable<T>[])

I don't understand why l1 compiles and l3 does not?

Is there a way to make all 3 work?

Nkosi
  • 235,767
  • 35
  • 427
  • 472
kofifus
  • 17,260
  • 17
  • 99
  • 173

1 Answers1

3

This has to do with how target-typed new works. From the proposal, it has the following feature:

A target_typed_new expression does not have a type. However, there is a new object creation conversion that is an implicit conversion from expression, that exists from a target_typed_new to every type.

During overload resolution of MyList's constructors, it looks at new("John", 30) and new("Eric", 30) and finds that there are conversions from those expressions, to both Employee and IEnumerable<Employee>, so both overloads are applicable, and neither is better. Hence the call is ambiguous.

In that sense, it's a bit like the null literal, or the default literal, and I suspect that l3 doesn't work because of a similar reason that this doesn't work:

var l3 = new MyList<Employee>(
    default,
    default
);

Though, the documentation also says:

The following are consequences of the specification:

  • ...
  • The following kinds of types are not permitted as targets of the conversion
    • ...
    • Interface types: This would work the same as the corresponding creation expression for COM types.
    • ...

and IEnumerable<Employee> is an interface.

Which suggests that the "whether the conversion is valid" is done after overload resolution, which is weird. But the fact that interfaces are invalid targets is only a "consequence" of the spec, and not explicitly specified (the conversion is specified to be valid to all types, but since target-typed new is a kind of object creation expressions, and you can't use interfaces in object creation expressions), so I guess it's technically fine.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • but `new("John", 30)` is definitely not an 'IEnumerable' (or any IEnumerable for that matter as it has different types) why or as what is it considered ? – kofifus Jul 03 '21 at 03:49
  • 1
    @kofifus During overload resolution, the compiler only looks at the available conversions the argument expressions has to the parameter types of the overloads. From the quote, conversions "exist from a target_typed_new to every type", so during overload resolution, the compiler thinks `new("John", 30)` can be converted to `IEnumerable`, and if overload resolution succeeds, it will _later_ find out that that won't work. – Sweeper Jul 03 '21 at 03:53