9.8 KiB
C# Language Design Meeting for June 14th, 2021
Agenda
Quote of the Day
- "My first reaction is that I thought we got rid of the C# test team for testing esoteric scenarios"
Discussion
Open questions in CallerArgumentExpressionAttribute
https://github.com/dotnet/roslyn/issues/52745#issuecomment-849961999
https://github.com/dotnet/csharplang/issues/287
VB Support
Conclusion: yes, we should support VB here.
Generated code
This question centers around this example:
void M([CallerMemberName]string arg1 = "1", [CallerArgumentExpression("arg1")]string arg2 = "2")
{
Console.WriteLine(arg2); // What gets printed?
}
void M2()
{
M();
}
As we see it, there are 5 possible values that a reasonable programmer could expect.
null
- The empty string:
""
. - The default value of
arg1
, as an expression:"\"1\""
. - The default value of
arg2
:"2"
. - The value filled in for
arg1
, as an expression:"\"M2\""
.
We don't think option 1 is useful here, as the parameter is attributed to not accept null
, and this
would just mean that every use of CallerArgumentExpression
would be required to handle the null
case.
We also don't think that options 3 or 5 are really correct either: the attribute here is about providing
the specific syntax the user used, not the value the user used. There are many ways to express the values
given as a constant value: we could just turn "M2"
into a string, or we could say "\"" + "M" + "2" + "\""
.
Both are technically correct, but neither reflects what the user actually wrote. Finally, for option 3, we
think that this is trying to second-guess the user. They provided a default value for the parameter, and if
we never respect that value then the default value was useless. Given these, we think the correct approach
is option 4.
Conclusion
Option 4: the default value of the parameter will be used. We will not turn compiler-generated code into equivalent C# expressions.
Self-referential arguments
Consider these examples:
void M3([CallerArgumentExpression("arg1")]string arg1 = ""); // Warning?
M3(); // What gets passed? null? ""?
Conclusion
We think this is absolutely worth a warning in source code, and if in metadata then we should just provide the default value of the parameter.
Span of the expression
Consider this example:
M(arg1: /* Before */ "A" + /* Mid */ "B"
/* After */); // What is passed for arg2?
There are 3 possible answers for this:
- The argument expression should refer to the start
arg1:
to the end of the position, either)
or,
, depending on whether the argument is followed by another or not. - The argument expression should refer from just after
arg1:
to the end of the position, not including the argument specifier. - We should ignore any trivia, and just have the expression span from the start of the real C# executable code
(the string
"A"
) to the end of the real executable C# code (the end of"B"
).
While there are legitimate argument for 1 or 2, we don't think they provide enough benefit to make up for the fact that they will be including leading and trailing whitespace that we don't believe is useful for the users of this attribute. Given this, we think option 3 is the correct way forward.
Conclusion
Option 3: we go from the start of real C# executable code to the end of the expression, not including any leading or trailing trivia.
List pattern syntax
https://github.com/dotnet/csharplang/issues/3435
Revisiting syntax
We've heard a lot of community feedback around our existing proposal for length patterns, which looks like this:
_ = list is [0]; // List has length 0;
Top among user feedback is that this syntax is:
- Confusing. Even among users who tend to give the LDM the benefit of the doubt with syntax choices, we've heard vociferous feedback that this is not clear and that there is not a clear enough parallel to array creation length specifiers to make this obvious.
- Unnatural for the base case. The traditional recursive pattern that languages with strong pattern matching
constructs use is some number of cases that pull out interesting bits, and then a base case to handle the empty list.
Unfortunately,
{ }
is not the empty list case, despite being what otherwise appears to be an empty list pattern. While in some cases this happens to work because all that's left to handle is when the input is non-null, we don't think it will lead to clear code.
A smaller group met to try and brainstorm some approaches to solving the issue. These are:
- Return to the original proposal syntax, using square brackets (
[]
) to denote a list pattern. This breaks with the correspondence principle, but it does have stronger parallel with other languages, has a natural base case, and we could potentially add a new creation form that achieves correspondence (and take the time to address things likeImmutableArray<T>
, which cannot be initialized by collection initializers today). - Use a separator at the end of a list pattern, such as
;
:{ 1, 2, 3; } or { ; }
. This separator would be required, giving a few advantages:- Because the separator is always required, the base case looks like the shortest version of the pattern.
- Allows list and property patterns to be combined into a single block.
- Gives us an avenue to allow collection and property initializers in the same block, by reusing the same syntax later.
- Keep the status quo. Users will get used to the syntax.
These suggestions led to spirited debate. An unfortunate truth here is that, no matter what approach we take, we have
discrepency with some aspect of the language. The semicolon separator approach allows us to mostly keep in line with
collection initializers, but the trailing ;
being required is very different and a wart. Square brackets, on the other
hand, are very different from the rest of C#. Today, square brackets are used for indexing operations and for
specifying the length of an array. Nothing in C# uses them to denote a group of things that is a collection. There are
proposals to use these brackets for an improved version of collection initializers though, giving us an opportunity for
future fulfillment of the correspondence principle, even if it won't be fulfilled on initial release. Patterns also
already have some discrepency with the rest of C#, particularly around and/or/not
patterns, which aren't words used
in the rest of the language.
Conclusion
We will go with option 1: using square brackets for the list pattern. We still need to decide if and how these can be combined with recursive patterns, but it gives us the most flexibility with regard to future regularity in the language.
Length patterns
Orthogonally, we have also come up with a few suggestions for the length pattern:
- Recognize a special
length
keyword as a property pattern:{ length: 10 }
. When a type is Countable, this property is available, and it will bind toLength
orCount
as appropriate. - Recognize the
Length
andCount
properties on:- Types that are countable
- Types that are both countable and indexable
- Keep the status quo.
Given that we've chosen square brackets for our new list pattern syntax, option 3 is out. This leaves us with option
1 or 2. We originally wanted special length patterns in the language because we wanted list patterns to work on a type
that didn't have a Length
or Count
property: IEnumerable
. While we still want to do this, the implementation work
is quite complex and we think that it might not get into the initial version. So, while we're not ruling out 1, we don't
think it's necessary quite yet.
Option 2 is nice, but it has a couple of wrinkles. First, it's a breaking change, because we specially recognize that
the property in question cannot be negative. This can affect flow analysis and introduce warnings or errors about
unreachable patterns, and remove warnings about non-exhaustive switch expressions. It's not pretty, but we think we can
tie this recognition to a warning wave. It will be the first time a warning wave removes warnings, instead of adding
them, but we think it's the right move. Second, what types should we specially recognize here. Countable is a very broad
definition in C#: it pretty much just means has an accessible property named either Count
or Length
. We think that's
too broad for general recognition; while collections should never have negative lengths, the word Count
or Length
on
its own is not strong enough evidence that the type is a collection. Instead, we think we should require both countable
and indexable, the same requirements for using a list pattern in the first place. This will ensure that the type at least
behaves like a collection, and while there still might be such types that return negative Count
s or Length
s, patterns
are only one place where such types will confuse their users and we don't think it's an edge case that should derail the
whole feature.
Conclusion
We will specially recognize the Count
and Length
properties on types that are both countable and indexable, assuming
that it can never be negative.
Timing
Given that the changes we've made today are specifically driven by community feedback, we feel that this feature needs more bake time than is left in the C# 10 cycle. The feature will ship in preview, either with C# 10 (like static abstracts in interfaces) or shortly after 10 is released. We want to make sure that the course-corrections we're making here help community understanding of the feature, and we don't have enough time before C# 10 is released to implement the changes and get them in customer hands before 10 is declared final.