In many situations, there is only one possible type allowed in a given place. And yet C# still requires you to explicitly list the type. Now that the Target-typed `new` expression proposal has been adopted into C# 9, such boilerplate code will no longer be necessary.
If this opening paragraph sounds familiar, it was because we talked about this proposal in January of last year. At that time, a prototype of target-typed `new` expressions was part of C# 8. It didn’t make the cut, but since then work has continued and now its status is “Merged into 16.7p1”.
If you’re already familiar with the feature, nothing has changed in terms of overall design. In fact, the syntax hasn’t really changed since it was under consideration for C# 7.1 in 2017. For those who haven’t seen it before, basically it is the opposite of the var keyword. Instead of omitting the type name on the variable declaration, you omit the type name on the value creation side. Here are a couple of examples,
private Dictionary<string, List<int>> field = new Dictionary<string, List<int>>();
private Dictionary<string, List<int>> field = new();
XmlReader.Create(reader, new XmlReaderSettings() { IgnoreWhitespace = true });
XmlReader.Create(reader, new() { IgnoreWhitespace = true });
From the developer’s perspective, that’s pretty much all there is to it. The feature removes the type in situations where it is either redundant or simply not interesting. But from a language design perspective there are numerous issues to be considered.
For example, what should occur if there are two viable overloads? Should the compiler choose the “best” match, or mark it as an ambiguous error as it does for two overloads that differ only on the type of an out parameter?
According to LDM notes, Microsoft chose the latter. Part of the reason is to make adding new overloads less likely to result in a breaking change. Note the phrase “less likely”, as this kind of type inference will always be susceptible to issues caused by additional overloads.
A common language design issue is determining when to filter out inappropriate overloads. In the past there have been cases where the compiler would choose one overload, only to later issue a compiler error because it violated a generic parameter constraint. This is known as a “late filter approach” and while it simplifies the compiler design, it reduces the chances that the compiler will successfully find an overload in an arbitrary piece of code.
An “early filter approach” would instead try to eliminate as many overloads as possible before choosing one. Again, this increases the complexity of the compiler in exchange for being more likely to find a good match. Here’s an example from the LDM notes.
struct S1 { public int x; }
struct S2 {}
M(S1 s1);
M(S2 s2);
M(new () { x = 43 }); // ambiguous with late filter, resolved with early.
With an early filter approach, the compiler will see that S1
doesn’t have a field named x
so it will eliminate it as a possible candidate. Using the late filter approach, the compiler only looks at constructor parameters before making a determination.
As you can imagine, the early filter scenario could grow to become quite complicated when you start nesting constructors. So as of that LDM, Microsoft chose to use the late filter approach.
According to the same LDM, the following scenarios are not supported because they are “unconstructible types”:
- pointer types
- array types
- abstract classes
- interfaces
- enums
Enums are excluded because there’s no benefit for using SomeEnum x = new()
when SomeEnum x = 0
or SomeEnum x = default
is clearer. And abstract types obviously can’t be constructed, but interfaces are actually more interesting than they may appear.
Though most people are unaware of it, C# does support the concept of a default implementation class for interfaces. Xenoprimate provided this example,
[Guid("899F54DB-5BA9-47D2-9A4D-7795719EE2F2")]
[ComImport()]
[CoClass(typeof(FooImpl))]
public interface IFoo
{
void Bar();
}
public class FooImpl : IFoo
{
public void Bar()
{
Console.WriteLine("XXXX");
/* ... */
}
}
public static void Main()
{
var foo = new IFoo(); // works just fine
foo.Bar();
}
As this is a very obscure scenario, it is understandable that Microsoft decided not to consider it when designing the target-typed `new` expression feature.
Another question that came up is whether or not to allow throw new(). In theory this could just throw a raw Exception, but since throwing Exception is considered to be a bad practice, the syntax will not be supported.