csharplang/meetings/2021/LDM-2021-09-20.md
2021-09-21 09:53:30 -07:00

7.4 KiB

C# Language Design Meeting for September 20th, 2021

Agenda

  1. Lambda breaking changes
  2. Newlines in non-verbatim interpolated strings
  3. Object initializer event hookup
  4. Type alias improvements

Quote of the Day

  • "You're kicking in an open door"

Discussion

Lambda breaking changes

https://github.com/dotnet/roslyn/pull/56341

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.

Breaking changes around overload resolution

https://github.com/dotnet/roslyn/issues/55691
https://github.com/dotnet/roslyn/issues/56167
https://github.com/dotnet/roslyn/issues/56319
https://github.com/dotnet/csharplang/discussions/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:

  1. Leave the breaking changes as-is.
  2. Change “method type inference” and “best common type” to not infer from the natural type of a lambda expression or method group.
  3. Change “better function member” to treat delegate types with identical signatures as equivalent, allowing tie-breaking rules to apply.
  4. 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.
  5. Change “better function member” to prefer parameter types that are delegate types other than those used for natural type. (Prefer D over Action.)
  6. Change “better function member” to prefer argument conversions other than “function type” conversions.
  7. Change “better function member” to prefer parameter types D or Expression<D> over Delegate or Expression, where D 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.

Conclusion

Options 4+6 accepted.

Method groups converting to Expression

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.

Conclusion

No changes will be made.

Lambdas in OHI

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.

Conclusion

No changes.

Newlines in non-verbatim interpolated strings

https://github.com/dotnet/csharplang/issues/4935

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.

Conclusion

Language change approved.

Object initializer event hookup

https://github.com/dotnet/csharplang/issues/5176

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.

Conclusion

Approved. We want to explore even more enhancements in this space.

Type alias improvements

https://github.com/dotnet/csharplang/issues/4284

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:

  1. 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.
  2. 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.

Conclusion

Overall, we like the semantic meaning. We still need to consider whether aliases should be allowed to have top-level nullability.