- Lambda breaking changes
- Newlines in non-verbatim interpolated strings
- Object initializer event hookup
- Type alias improvements
- "You're kicking in an open door"
In the ever continuing saga of new breaking changes introduced by giving method groups and lambda expressions natural types, we looked at a few new breaking changes today to decide what, if any, workarounds we should adopt to try and fix them.
dotnet/roslyn#55691
dotnet/roslyn#56167
dotnet/roslyn#56319
#5157
We have a number of reports from users who have been broken by changes in overload resolution, mostly because a set of overloads that used to succeed in overload resolution are now ambiguous. A smaller group met to discuss a number of different potential solutions to the issue. These options were:
- Leave the breaking changes as-is.
- Change “method type inference” and “best common type” to not infer from the natural type of a lambda expression or method group.
- Change “better function member” to treat delegate types with identical signatures as equivalent, allowing tie-breaking rules to apply.
- Change “better function member” to prefer overloads where method type inference did not infer type arguments from the natural types of lambdas or method groups.
- Change “better function member” to prefer parameter types that are delegate types other than those used for natural type. (Prefer
D
overAction
.) - Change “better function member” to prefer argument conversions other than “function type” conversions.
- Change “better function member” to prefer parameter types
D
orExpression<D>
overDelegate
orExpression
, whereD
is a delegate type.
Discussion further narrowed our focus to two combinations of the above options: 3+7 or 4+6. 3+7 results in a more aggressive break, while 4+6 is more compatible with previous versions of C#. Given the extent of some of the breaks we're seeing, we think the more compatible approach is the better way to go, so we'll proceed with the PR linked at the start of this section.
Options 4+6 accepted.
Another break testing has revealed looks like this:
var c = new C();
c.M(F); // C#9: E.M(); C#10: error CS0428: Cannot convert method group 'F' to 'Expression'.
static int F() => 0;
class C
{
public void M(Expression e) { Console.WriteLine("C.M"); }
}
static class E
{
public static void M(this object o, Func<int> a) { Console.WriteLine("E.M"); }
}
We think we would have a solution for this: split our "function type conversion" into two separate conversion types: a function type
conversion from lambda, and a function type conversion from method group. Only the former would have a conversion to Expression. This
would make it so that M(Expression)
is not applicable if the user passed a method group, leaving only M(object, Func<int>)
. This
could be a bit complex, but it should resolve the issue.
Unlike the previous examples, however, we don't have any reports of this issue. Given the number of reports of the previous breakages we've received, and the lack of reports for this issue, we tentatively think that it's not worth fixing currently. If, after we ship C# 10 for real, we received reports of this break, we know how to fix it and can change course at that time without making a breaking change.
No changes will be made.
A final break we looked at today is:
using System;
B.F1(() => 1); // C#9: A.F1(); C#10: B.F1()
var b = new B();
b.F2(() => 2); // C#9: A.F2(); C#10: B.F2()
class A
{
public static void F1(Func<int> f) { }
public void F2(Func<int> f) { }
}
class B : A
{
public static void F1(Delegate d) { }
public void F2(Delegate d) { }
}
This is standard OHI behavior in C#, but because the derived overloads were previously not applicable, they were not included in the
B.F1
or b.F2
method groups, and only the methods from A
would be applicable. Now that methods from the more derived type are
applicable, methods from the base type are filtered out by method group resolution.
We think this is both fine and actually desirable behavior. We don't have contravariant parameters in C#, but this is effectively acting like such, which is a good thing. This change is also not customer-reported, but was instead discovered in testing. Given the desirable behavior and lack of reports, we think no change is necessary.
No changes.
We have a lot of compiler complexity around ensuring interpolated strings do not have a newline in them, and we don't see a real reason to forbid newlines. We think the origin might have come from the number of different design flip-flops we made on interpolated strings during their initial design.
Language change approved.
LDM is not only interested in this change, we're also interested in generalized improvements that can be made in object initializers
and with expressions. Compound assignment is interesting, particularly in with
expressions, and we would like to see what improvements
we could make not just for events, but for all types of properties and fields.
Approved. We want to explore even more enhancements in this space.
Finally today, we looked at one of the open questions in this proposal: how should we handle nullable types in using aliases when the
alias is used in a #nullable disable
location.
There are largely 2 ways to view using aliases:
- Syntactic substitutions: the compiler is literally copy/pasting the thing in the alias into the target location. In this view, the compiler should treat the syntax as occuring at the use point, and warn based on that.
- Semantic substitutions: the using alias is effectively defining a new type. It's not a truly different type, but only the meaning is substituted, not the actual syntax. If we ever want to consider a way to export using aliases, this will be a useful meaning to assume.
We also have some (possibly unintended) prior art here: using MyList = System.Collections.Generic.List<string?>;
takes the second
approach today, acting like a semantic substitution.
The one thing we still want to consider in this space is top-level nullability. We're not sure about allowing a type alias to have
top-level nullability when it's an alias to a reference type. There is (very intentionally) no extra C# syntax for "not null reference
type" beyond the lack of a ?
, and the next ask if we were to allow aliases to be top-level nullable would be for such a syntax.
Overall, we like the semantic meaning. We still need to consider whether aliases should be allowed to have top-level nullability.