- "I was kinda hoping you'd fight me on this"
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
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:
- Should we synthesize a constructor to run the field initializer? and
- 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.
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.
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:
- Parameterless primary constructors are allowed on struct types.
- 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.
Use the above rules.
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.
We won't attempt to be smart here. Users can provider their more customized equality implementation if they so choose.
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.
We don't do anything smart here. Users can just mark the struct as readonly
.
We're getting close the end of open questions in this proposal. We looked at 2 today:
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 bool
s 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.
Using the attribute is accepted. The Append
and Create
method signatures will drive the lowering process.
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.
We won't pursue specific structured syntax at this time.