127 строки
10 KiB
Markdown
127 строки
10 KiB
Markdown
|
|
# C# Language Design Meeting for July 6th, 2020
|
|
|
|
## Agenda
|
|
|
|
1. [Repeated attributes on `partial` members](#repeated-attributes-on-partial-members)
|
|
2. [`sealed` on `data` members](#sealed-on-data-members)
|
|
3. [Required properties](#required-properties)
|
|
|
|
## Quote of the Day
|
|
|
|
"Is there a rubber stamp icon I can use here?"
|
|
|
|
## Discussion
|
|
|
|
### Repeated attributes on `partial` members
|
|
|
|
https://github.com/dotnet/csharplang/pull/3646
|
|
|
|
Today, when we encounter attribute definitions among partial member definitions, we merge these attributes, applying attributes multiple
|
|
times if there are duplicates across the definitions. However, if there are duplicated attributes that do not allow multiples, this merging
|
|
results in an error that might not be resolvable by the user. For example, a source generator might copy over the nullable-related
|
|
attributes from the stub side to the implementation side. This is further exacerbated by the new partial method model: previously, the
|
|
primary generator of partial stubs was the code generator itself. WPF or other code generators would generate the partial stub, which the
|
|
user could then implement to actually create the implementation. These generators generally wouldn't add any attributes, and the user could
|
|
add whatever attributes they wanted. However, the model is flipped for the newer source generators. Users will put attributes on the stub in
|
|
order to hint to the generator how to actually implement the method, so either the generator will have never copy attributes over (possibly
|
|
complicating implementation), or it will have to be smart about only copying over attributes that matter. It would further hurt debugability
|
|
as well; presumably tooling will want to show the actual implementation of the method when debugging, and it's likely that the tooling won't
|
|
want to try and compute the merged set of attributes from the stub and the implementation to show in debugging.
|
|
|
|
What emerged from this discussion is a clear divide in how members of the LDT view the stub and the implementation of a partial member: some
|
|
members view the stub as a hint that something like this method exists, and the implementation provides the final source of truth. This group
|
|
would expect that, if we were designing again, all attributes would need to be copied to the implementation and attributes on the stub would
|
|
effectively be ignored. The other segment of the LDT viewed partial methods in exactly the opposite way: the stub is the source of truth, and
|
|
the implementation had better conform to the stub. This reflects these two worlds of previous source generators vs current generators: for the
|
|
previous uses of partial, the user would actually be creating the implementation, so that's the source of truth. For the new uses, the user is
|
|
creating the stubs, so that's the source of truth.
|
|
|
|
We also briefly considered a few ways of enabling the attribute that keys the generator to be removed from the compiled binary, so that it
|
|
does not bloat the metadata. However, we feel that that's a broader feature that's been on the backlog for a while, source-only attributes. We
|
|
don't see anything in this feature conflicting with source-only attributes. We also don't see anything in this feature conflicting with
|
|
future expansions to partial members, such as partial properties.
|
|
|
|
#### Conclusion
|
|
|
|
The proposal is accepted. For the two open questions:
|
|
|
|
1. We will deduplicate across partial types as well as partial methods if `AllowMultiple` is false. This is considered lower priority if a
|
|
feature needs to be cut from the C# 9 timeframe.
|
|
2. We don't have a good use-case for enabling `AllowMultiple = true` attributes to opt into deduplication. If we encounter scenarios where
|
|
this is needed, we can take it up again at that point.
|
|
|
|
### `sealed` on `data` members
|
|
|
|
In a [previous LDM](LDM-2020-06-22.md#data-properties), we allowed an explicit set of attributes on `data` members, but did not include
|
|
`sealed` in that list, despite allowing `new`, `override`, `virtual`, and `abstract`. `sealed` feels like it's missing, as it's also
|
|
pertaining to inheritance.
|
|
|
|
#### Conclusion
|
|
|
|
`sealed` will be allowed on `data` properties.
|
|
|
|
### Required properties
|
|
|
|
https://github.com/dotnet/csharplang/issues/3630
|
|
|
|
In C# 9, we'll be shipping a set of improvements to nominal construction scenarios. These will allow creation of immutable objects via
|
|
object initializers, which has some advantages over positional object creation, such as not requiring exponential constructor growth
|
|
over object hierarchies and the ability to add a property to a base class without breaking all derived types. However, we're still
|
|
missing one major feature that positional constructors and methods have: requiredness. In C# today, there is no way to express that a
|
|
property must be set by a consumer, rather than by the class creator. In fact, there is no requirement that all fields of a class type
|
|
need to be initialized during object creation: any that aren't initialized are defaulted today. The nullable feature will issue warnings
|
|
for initialized fields inside the declaration of a class, but there is no way to indicate to the feature that this field must be initialized
|
|
by the consumer of the class. This goes further than just the newly added `init`: mutable properties should be able to participate in this
|
|
feature as well. In order for staged initialization to feel like a true first-class citizen in the language, we need to support requiredness
|
|
in the contract of creating a class via the feature.
|
|
|
|
The LDM has seemingly universal support of making improvements here. In particular, the proposed concept of "initialization debt" resonated
|
|
strongly with members. It allows for a general purpose mechanism that seems like it will extend natural to differing initialization modes.
|
|
We categorized the two extreme ends of object initialization, both of which can easily be present in a single nominal record: Nothing is
|
|
initialized in the constructor (the default constructor), and everything is initialized in the constructor (the copy constructor). The next
|
|
question is how are these initialization contracts created: there's some tension here with the initial goal of nominal construction.
|
|
|
|
Fundamentally, initialization contracts can be derived in one of two ways: implicitly, or explicitly. Implicit contracts are attractive at
|
|
first glance: they require little typing, and importantly they're not brittle to the addition of new properties on a base class, which was
|
|
an important goal of nominal creation in the first place. However, they also have some serious downsides: In C# today, public contracts for
|
|
consumers are always explicit. We don't have inferred field types or public-facing parameter/return types on methods/properties. This means
|
|
that any changes to the public contract of an object are obvious when reviewing changes to that type. Implicit contracts change that. It
|
|
would be very possible for a manually-implemented copy constructor on a derived type to miss adding a copy when a property is added to its
|
|
base type, and suddenly all uses of `with` on that type are now broken.
|
|
|
|
We further observe that this shouldn't just be limited to auto-properties: a non-autoprop should be able to be marked as required, and then
|
|
any fields that the initer or setter for that property initializes can be considered part of the "constructor body" for the purposes of
|
|
determining if a field has been initialized. Fields should be able to be required as well. This could extend well to structs: currently,
|
|
struct constructors are required to set all fields. If they can instead mark a field or property as required then the constructor wouldn't
|
|
have to initialize it all.
|
|
|
|
One way of implementing initialization debt would be to tie it to nullable: it already warns about lack of initialization for not null
|
|
fields when the feature is turned on. We're still in the nullable adoption phase where we have more leeway on changing warnings, so we
|
|
could actually change the warning to warn on _all_ fields, nullable or not. This would effectively be an expansion of definite assignment:
|
|
locals must be assigned a value before use, even if that value is the default. By extending that requirement to all fields in a class, we
|
|
could essentially make all fields required when nullable is enabled, regardless of their type. Then, C# 10 could add a feature to enable
|
|
skipping the initialization of some members based on whether the consumer must initialize them. This is also not really a breaking change
|
|
for structs: they're already required to initialize all fields in the constructor. However, it would be a breaking change for classes, and
|
|
we worry it would be a significantly painful one, especially with no ability to ship another preview before C# 9 releases. `= null!;` is
|
|
already a significant pain point in the community, and this would only make it worse.
|
|
|
|
We came up with a few different ways to mark a property as being required:
|
|
* A keyword, as in the initial proposal, on individual properties.
|
|
* Assigning some invalid value to the field/property. This could work well as a way to be explicit about what fields a particular
|
|
constructor would require, but does leave the issue about inherited brittleness.
|
|
* Attributes or other syntax on constructor bodies to indicate required properties.
|
|
|
|
We like the idea of some indication on a property itself dictating the requiredness. This puts all important parts of a declaration together,
|
|
enhancing readability. We think this can be combined with a defaulting mechanism: the property sets whether it is required, and then a
|
|
constructor can have a set of levers to override individual properties. These levers could go in multiple ways: a copy constructor could
|
|
say that it initializes _all_ members, without having to name individual members, whereas a constructor that initializes one or two specific
|
|
members could say it only initializes those specific members, and inherits the property defaults from their declarations. There's still
|
|
open questions in this proposal, but it's a promising first start.
|
|
|
|
#### Conclusion
|
|
|
|
We have unified consensus that in order for staged initialization to truly feel first-class in the language, we need a solution to this issue,
|
|
but we don't have anything concrete enough yet to make real decisions. A small group will move forward with brainstorming and come back to
|
|
LDM with a fully-fleshed-out proposal for consideration.
|