csharplang/meetings/2021/LDM-2021-04-28.md
2021-07-27 12:34:38 -07:00

8 KiB

C# Language Design Meeting for April 28th, 2021

Agenda

  1. Open questions in record and parameterless structs
  2. Improved interpolated strings

Quote of the Day

  • "I was kinda hoping you'd fight me on this"

Discussion

Open questions in record and parameterless structs

https://github.com/dotnet/csharplang/blob/struct-ctor-more/proposals/csharp-10.0/parameterless-struct-constructors.md
https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/record-structs.md

Initializers with explicit constructors

The first thing we need to resolve today is a question around our handling of field initializers, both in the presence of an explicitly-declared constructor(s), and when no constructors are present. There's two parts to this question:

  1. Should we synthesize a constructor to run the field initializer? and
  2. If we don't, should we warn the user that their field initializer won't be run?

We feel that a simple rule here would be to mirror the observed behavior with reference types: if there is no explicit constructor for a struct type, we will synthesize that constructor. If that constructor happens to be empty (as it would be today in a constructorless struct type because field initializers aren't yet supported) we optimize that constructor away. If a constructor is explicitly declared, we will not synthesize any constructor for the type, and field initializers will not be run by new S() unless the parameterless constructor is also explicitly declared. This does have a potential pit of failure where users would expect the parameterless constructor to run the field initializers, but synthesizing a parameterless constructor would have bad knock-on effects for record structs with a primary constructor: what would the parameterless constructor do there? It doesn't have anything it can call the primary constructor with, and would result in confusing semantics. To address this potential issue, we think there is room for a warning wave dedicated to using new S(), when S does not have a parameterless constructor. There will be some holes in this, particularly around generics: struct today implies new(), and we're concerned about how breaking the change would be if we tried to make the warning apply to everywhere that a parameterless struct was substituted for a type parameter constrained to struct. We would also have to take another language feature to enable where T : struct, new(), which isn't allowed today. If there is future appetite for introducing another warning wave to cover the generic hole, we can look at it at that point.

Conclusion

We will only synthesize a constructor when the user does not explicitly declare one. We will consider a warning wave when using new on a struct that does not have a parameterless constructor and also has an explicit constructor with parameters.

Record struct primary constructors

Next, we looked at how the parameterless struct constructor feature will interact with record primary constructors. The rule we decided for the first question ends up making the decisions very simple here:

  1. Parameterless primary constructors are allowed on struct types.
  2. The rules for whether an explicit constructor needs to call the primary constructor are the same as in record class types. This applies to an explicit primary constructor too, just like in record class types.
Conclusion

Use the above rules.

Generating the Equals method with in

We looked at a potential optimization around the Equals method, where we could generate it with an in parameter, instead of with a by-value parameter. This could help scenarios with large struct types get better codegen. However, when we would want to do this is a very complicated heuristic. For structs smaller than machine pointer size, it is usually faster to pass by value. This gets even more complex when considering types that are passed in non-standard registers, such as vectorized code. We don't think there's a generalized way to do this heuristic. Instead, we just need to make sure that the user can perform this optimization, if they want to.

Conclusion

We won't attempt to be smart here. Users can provider their more customized equality implementation if they so choose.

readonly Equality and GetHashCode

Finally in record structs, we looked at automatically marking the Equals and GetHashCode methods as readonly, if the all the fields they use for the calculation are also all readonly. While this would be technically feasible, we're not sure what the scenario for this is, beyond just marking the entire struct readonly. At that point, every method would be readonly, including the ones we synthesize.

Conclusion

We don't do anything smart here. Users can just mark the struct as readonly.

Improved interpolated strings

https://github.com/dotnet/csharplang/issues/4487

We're getting close the end of open questions in this proposal. We looked at 2 today:

Confirming InterpolatedStringBuilderAttribute

An internal discussion on simplifying the conversion rules resulted in a proposal that we only look for the presence of a specific attribute on a type to determine if there exists a conversion from an interpolated string literal to the type. This simplification results in much cleaner semantics: the presence of various methods on the builder type no longer plays into whether the conversion exists, only whether conversion is valid. This will help users get understandable errors that don't silently fall back to string-based overloads when a builder doesn't have the right set of Append methods to lower the interpolated string.

We also looked at whether we should control the final codegen based on a property on the attribute: we initially proposed conditional evaluation could be controlled by the attribute. This would potentially let the compiler change the behavior of when expressions in interpolation holes are evaluated: up front, or in line the Append calls. However, the logic for determining the codegen is not as simple as one property: the Append calls can potentially return bools to stop evaluation, and the Create method can potentially out a parameter to control this as well. There are valid scenarios for all 4 possibilities here, so a switch that only allows either all conditional or no conditional isn't a good option. It's also complex because this would be a library author making a lowering decision for user code. We also note that, if we decide that this is important at a later date, we can extend the pattern then.

Conclusion

Using the attribute is accepted. The Append and Create method signatures will drive the lowering process.

Specific extensions for structured logging

Finally today, we looked at a question around including specific syntax to support structured logging. Message templates are a well-known structured logging format that most of the biggest .NET logging frameworks support, and as we want these libraries to consider using the improved interpolated strings feature, we looked to see if we can include specific syntax to help encourage this. After some initial discussion, our general sentiment is that this feels too narrow. An example syntax we considered is $"{myExpression#structureName,alignment:format}", and while this would work for the scenario, it wouldn't be a meaningful improvement over simply using a tuple expression: $"{("structureName", myExpression),alignment:format}". It is possible to construct interpolated string builders that only accept tuples of string and T in their interpolation holes, and with the other adjustments we made today there should be good diagnostics for such cases. Further restrictions can be imposed via analyzers, as is possible today. While a more general string templating system could be interesting, we think that this is a bit too narrow of a focus for a language feature today.

Conclusion

We won't pursue specific structured syntax at this time.