- "No fingerprints today, I always struggle with it after a bank robbery, when I file them down"
We had 3 open questions from tests on list patterns. Questions 1 and 2 both relate to types with both Length
and Count
properties, so our decision
applies to both.
Our previous definition of a type that can be matched by a list pattern is that the type must be both indexable and countable. Because countable could be
one of two properties, Length
or Count
, we need to decide what do when a type has both. Our options are:
- Treat them as equivalent and assume they return the same value. This will affect subsumption.
- Treat the non-recognized one (
Count
in the case where bothLength
andCount
are defined) as not being special at all.
Conceptually, we think it would be odd to assume that Count
is equal to Length
, even though we'll never use it for any form of slicing or length checking.
While it might be reasonable to assume they return the same value, we don't think it's an important scenario. We'll have nearly a year of preview on this
feature, so we have plenty of time to react to feedback if dogfooding shows that it's an important scenario.
We will not treat Length
and Count
as being connected when both are defined.
We believe that well-defined slice methods should never return null
for an in-bounds range, and we'll never generate a call to such a slice method with
an out-of-bounds range. However, we also don't think there's a customer for this scenario: a search of GitHub showed no Slice
methods that return an
annotated value, and treating a slice method differently than its annotation would require both implementation effort and customer education. Given that
there are no known cases this affects, we don't think it's worth it. Similarly to the first question, if such scenarios are identified during preview, we
can correct appropriately.
If a Slice
method is annotated, we will treat it as potentially returning null
for the purposes of subsumption and exhaustiveness.
Now that records have been out for a year and we've expanded them to struct and reference types, we think it's time to start looking at primary constructors again. We updated the proposal with the new syntax forms from our records work, removing or modifying components where they make sense. Unlike for records, primary constructors aren't about defining the members that make up a grouping of data: instead, they're purely for defining the inputs to a class. So things like deconstruction, equality, and public properties aren't automatically generated from the parameters. Instead, they become fields, that are then eliminted if the field is never used outside of a field initializer.
We think there are a couple of open discussion topics:
- Should the fields be readonly or not?
- How important are primary constructor bodies for a generalized feature?
For question 1, we think that, for better or for worse, C# is a mutable-by-default language. We were able to change the defaults for record
types because
mutability in a value-based reference type is actively harmful, but we have to consider a number of naming and mutability issues we were able to skirt around
in record types. Field naming conventions, for example, are heavily split in C#, with some users using a leading _
, and some preferring to just use pascalCase
with no leading modifiers. We think readonly
is similar: if users would like to modify the defaults of C#, they can define their own field and do so. If this
proves to be the wrong default we can make a change before it ships for real, or potentially invest in #188 to allow
putting modifiers on parameters.
For question 2, we think that it will be important, but that once #2145 is in the language, a good portion of initialization code will be expressable in just a single initialization expression. It will miss some more complex scenarios, but we think just primary constructors are a good first step.
In general, we also want to make sure we're defining the difference between primary constructors and required properties well. With the potential for multiple new initialization features to ship in a single language version, we want to make sure they complement each other well. We think required properties work well for public contracts: DTOs, POCOs, and other similar, nominal data structures that will expose these properties externally. Primary constructors, on the other hand, are for other types, that do not define public surface area based on their parameters. Instead, they simply define input parameters, and can assign them to private fields or otherwise manipulate them as they choose.
Some concrete feedback on the proposal:
- The exception for calling
this
for copy constructors feels premature, as they don't mean anything to any type except record types. If we generalizewith
in the future, then the exception will make sense, and we can add it then. - We should disallow methods with the same name as primary constructor parameters. This is confusing and while it could potentially be understandable code if the
parameter was never captured, could then have spooky action at a distance if the parameter was accidentally captured when a user forgot to write the
()
of the method name.
These words are accepted.