csharplang/meetings/2020/LDM-2020-07-06.md
2020-07-08 13:26:15 -07:00

10 KiB

C# Language Design Meeting for July 6th, 2020

Agenda

  1. Repeated attributes on partial members
  2. sealed on data members
  3. 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, 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.