196 lines
11 KiB
Markdown
196 lines
11 KiB
Markdown
# C# Language Design Meeting for July 27th, 2020
|
|
|
|
## Agenda
|
|
|
|
- [Warnings on types named `record`](#warnings-on-types-named-record)
|
|
- [`base` calls on parameterless `record`s](#base-calls-on-parameterless-records)
|
|
- [Omitting unnecessary synthesized `record` members](#omitting-unnecessary-synthesized-record-members)
|
|
- [`record` `ToString` behavior review](#record-tostring-behavior-review)
|
|
- [Behavior of trailing commas](#behavior-of-trailing-commas)
|
|
- [Handling stack overflows](#handling-stack-overflows)
|
|
- [Should we omit the implementation of `ToString` on `abstract` records](#should-we-omit-the-implementation-of-tostring-on-abstract-records)
|
|
- [Should we call `ToString` prior to `StringBuilder.Append` on value types](#should-we-call-tostring-prior-to-stringbuilder.append-on-value-types)
|
|
- [Should we try and avoid the double-space in an empty record](#should-we-try-and-avoid-the-double-space-in-an-empty-record)
|
|
- [Should we try and make the typename header print more economic](#should-we-try-and-make-the-typename-header-print-more-economic)
|
|
- [Reference equality short circuiting](#reference-equality-short-circuiting)
|
|
|
|
## Quote of the Day
|
|
|
|
"He was hung up on his principles!"
|
|
|
|
"I painted my tower all ivory."
|
|
|
|
## Discussion
|
|
|
|
### Warnings on types named `record`
|
|
|
|
We previously discussed warning in C# 9 when a type is named `record`, as it will need to be escaped in field
|
|
definitions in order to parse correctly. This is a breaking change being made in C# 9, but we didn't explicitly
|
|
record the outcome of the warning discussion. We also discussed going further here: we could make lower-cased
|
|
type names a warning with the next warning wave, under the assumption that lowercase names make good keywords.
|
|
For example, we could start warning on types named `async` or `var`. Those who want to forbid usage of those
|
|
features in their codebase have a solution in analyzers.
|
|
|
|
#### Conclusion
|
|
|
|
We will warn on types named `record` in C# 9. Further discussion will need to happen for lowercase type names
|
|
in general, as there are globalization considerations, and we feel that disallowing `record` might be a good
|
|
smoke test for reception to the idea in general.
|
|
|
|
### `base` calls on parameterless `record`s
|
|
|
|
We have a small hole in records today where this is not legal, and there is no workaround to making it legal:
|
|
|
|
```cs
|
|
record Base(int a);
|
|
record Derived : Base(1);
|
|
```
|
|
|
|
The spec as written today explicitly forbids this: "It is an error for a record to provide a `record_base`
|
|
`argument_list` if the `record_declaration` does not contain a `parameter_list`." However, there is good
|
|
reason to allow this, and while this design may have fallen out of the principles that were set out given
|
|
that a default constructor for a nominal record is not recognized as a primary constructor, this seems to
|
|
violate principles around generally making record types smaller. Further, with the spec as written today
|
|
it's not possible to work around this issue by giving `Derived` an empty parameter list, as parameter lists
|
|
are not permitted to be empty.
|
|
|
|
A solution to this problem is relatively straightforward: we make the default constructor of a nominal record
|
|
the primary constructor, if no other constructor was provided. This seems to mesh well with the existing rules,
|
|
and seems to work well with future plans for generalized primary constructors. We could consider 2 variants of
|
|
this: requiring empty parens on the type declaration in all cases, or only requiring them in cases where the
|
|
default constructor would otherwise be omitted (if another constructor was added in the `record` definition).
|
|
|
|
#### Conclusion
|
|
|
|
We'll allow empty parens as a constructor in C# 9. We're also interested in allowing the parens to be omitted
|
|
when a default constructor would otherwise be emitted, but if this doesn't make it in C# 9 due to time
|
|
constraints it should be fine to push back to v next.
|
|
|
|
### Omitting unnecessary synthesized `record` members
|
|
|
|
Some record members might be able to be omitted in certain scenarios if they're unneeded. For example, a
|
|
`sealed` `record` that derives from `System.Object` would not need an `EqualityContract` property, as it
|
|
can only have the one contract. We could also simplify the implementation of `ToString` if we know there
|
|
will be no child-types that need to call `PrintMembers`.
|
|
|
|
However, despite leading to simplified emitted code for these types, we're concerned that this will cause
|
|
other code to become more complicated. We'll need to have more complicated logic in the compiler to handle
|
|
these cases. Further, source generators and other tools that interact with records programmatically will
|
|
have to duplicate this logic if they need to interact with record equality or any other portion of code that
|
|
"simplified" by this mechanism. We further don't see a real concern for metadata bloat in this scenario,
|
|
as we aren't omitted fields from the record declaration.
|
|
|
|
#### Conclusion
|
|
|
|
There are likely more downsides than upsides to doing something different here. We'll continue to have this
|
|
be regular.
|
|
|
|
### `record` `ToString` behavior review
|
|
|
|
Now that we have a proposal and implementation for `ToString` on record types, we'll go over some standing
|
|
questions from initial implementation review. Prior art for this area in C# that we found relevant was
|
|
`ToString` on both tuples and anonymous types.
|
|
|
|
#### Behavior of trailing commas
|
|
|
|
There is a proposal to simplify the implementation of `ToString` by always printing trailing commas after
|
|
properties. This would remove the need for some bool tracking flags for whether a comma was printed as
|
|
part of a parent's `ToString` call. However, no prior art (in either C# or other languages) that we could
|
|
find does this. Further, it provokes a visceral reaction among team members as feeling unfinished, and that
|
|
it would be the first bug report we get after shipping it.
|
|
|
|
##### Conclusion
|
|
|
|
No trailing commas.
|
|
|
|
#### Handling stack overflows
|
|
|
|
There is some concern that `ToString` could overflow if a record type is recursive. While this is also true
|
|
of equality and hashcode methods on records, there is some concern that `ToString` might be a bit special
|
|
here, as it's the tool that a developer would use to get a print-out of the record to find the cyclic element
|
|
in the first place. There's examples in the javascript world of this being a pain point. However, any
|
|
solution that we come up with for records will have limitations: it wouldn't be able to protect against
|
|
indirect cycles through properties that are records but not records of the same type, or against cycles
|
|
in properties that are not records at all. In general, if a user makes a record type with a cyclic data
|
|
structure, this is a problem they're going to have to confront already in equality and hashcode.
|
|
|
|
##### Conclusion
|
|
|
|
We will not attempt any special handling here. We could consider adding a `DebuggerDisplay` with more special,
|
|
reflective handling at a later point in time if this proves to be a pain point, but we don't think it's
|
|
worth the cost in regular `ToString` calls.
|
|
|
|
#### Quoting/escaping values
|
|
|
|
Today, records do not attempt to escape `string`s when printing, which could result in potentially confusing
|
|
printing. However, we do have concerns here. First, none of our prior art in C# does this escaping. Second,
|
|
the framework also does not escape `ToString()` like this. Third, there's a question of what _type_ of
|
|
escaping we should do here. Would we want to print a newlines as the C# version, `\n`, or the VB version?
|
|
Further, we already have some handling the expression evaluator and debugger for regular strings and
|
|
anonymous types, to ensure that the display looks good there. Given that this is mainly about ensuring
|
|
that records appear well-formatted in the debugger, it seems like the correct and language-agnostic approach
|
|
is to ensure the expression evaluator knows about record types as well.
|
|
|
|
##### Conclusion
|
|
|
|
We won't do any quoting or escaping of string record members. We should make the debugger better here.
|
|
|
|
#### Should we omit the implementation of `ToString` on `abstract` records
|
|
|
|
We could theoretically omit providing an implementation of this method on `abstract` record types as it
|
|
will never be called by the derived `ToString` implementation. On the face of it, we cannot think of a
|
|
scenario that would be particularly helped by including the `ToString`, however we also cannot think of
|
|
a scenario that would be harmed by including it, and doing so would make the compiler implementation simpler.
|
|
|
|
##### Conclusion
|
|
|
|
Keep it.
|
|
|
|
#### Should we call `ToString` prior to `StringBuilder.Append` on value types
|
|
|
|
The concern with directly calling `Append` without `ToString` on the value being passed is that for
|
|
non-primitive struct types, this will cause the struct to be boxed, and the behavior of `Append` is
|
|
to immediately call `ToString` and pass to the `string` overload.
|
|
|
|
##### Conclusion
|
|
|
|
Do it.
|
|
|
|
#### Should we try and avoid the double-space in an empty record
|
|
|
|
The implementation currently prints `Person { }` for an empty record. This provokes immediate "unfinished"
|
|
reactions in the LDT, similar to the trailing commas above.
|
|
|
|
##### Conclusion
|
|
|
|
Should we remove it? YES! YES! YES! YES!
|
|
|
|
#### Should we try and make the typename header print more economic
|
|
|
|
Today, we call `StringBuilder.Append(nameof(type))` and `StringBuilder.Append("{")` immediately after
|
|
as 2 separate calls, which is another method call we could drop if we did it all in one. We feel that this
|
|
is sufficiently outside the language spec as to be a thing a compiler could do if it feels it appropriate.
|
|
From the compiler perspective, however, it could actually be bad change, as it would increase the size of
|
|
the string table, which is known to be an issue in some programs, while getting rid of one extra method call
|
|
in a method not particularly designed to be as fast as possible in the first place.
|
|
|
|
##### Conclusion
|
|
|
|
Up to implementors. However, we don't believe it to be a good idea or worth any effort.
|
|
|
|
### Reference equality short circuiting
|
|
|
|
Today, we have a slight difference between the equality operators `==`/`!=` and the implementation of the
|
|
`Equals(record)` instance method, in that the former compare reference equality first, before delegating
|
|
to the `Equals` method. This ensures that `null` is equal to `null` as a very quick check, and ensures we
|
|
only have to compare one operand to `null` explicitly in the implementation of the operators. The `Equals`
|
|
instance member then does not check this condition. However, this means that the performance characteristics
|
|
of these two members can be very different, with the operators being an order of magnitude faster on even
|
|
small record types since it has to do potentially many more comparisons. The proposal, therefore, is to
|
|
check reference equality in the `Equals` instance member as well, as the first element.
|
|
|
|
#### Conclusion
|
|
|
|
We will do this. We'll keep the reference equality check in the operators as well to ensure that `null == null`,
|
|
and add one at the start of the instance method. It does mean that the `==` operators will check reference
|
|
equality twice, but this is an exceptionally quick check so we don't believe it's a big concern.
|