Field initializers in structs can often lead to friction points for authors #5635
Replies: 5 comments 17 replies
-
I don't have a dog in this fight as much as some others because my usage of structs is usually limited to simple scenarios which are handled fine from earliest versions. Having said that, may I suggest that somebody who is intimately familiar with all possible permutations of struct initialization and construction, puts together a short document (in any format) that demonstrates all these use cases? Such as local vs array vs class field (I suspect last 2 are the same) etc., and struct field initializer vs constructors, etc. If something like this already exists, a link would be good. At this point I'm trying to understand the topic but it has become so convoluted I can't quite wrap my head around. I have a feeling some others may be in the same boat. |
Beta Was this translation helpful? Give feedback.
-
I think I would prefer that the compiler just always synthesized the no args constructor, and only warned on records which have a field initialiser and no no args constructor. Forcing the user to write it is just busy work. |
Beta Was this translation helpful? Give feedback.
-
I think that the confusion already exists here when it comes to inconsistencies between structs vs classes in general but more than that having to write anything for that although might be necessary is confusing too. |
Beta Was this translation helpful? Give feedback.
-
What if we just said, struct MagnitudeVector3d
{
public double X, Y, Z;
public double Magnitude = 1;
}
var m = new MagnitudeVector3d(); // OK
Console.Write(m.X/Y/Z); // writes 0
Console.Write(m.Magnitude); // writes 1
struct MagnitudeVector3d
{
public MagnitudeVector3d()
{
this.X = 2;
}
public double X, Y, Z;
public double Magnitude = 1;
}
var m = new MagnitudeVector3d(); // OK
Console.Write(m.X); // writes 2
Console.Write(m.Y/Z); // writes 0
Console.Write(m.Magnitude); // writes 1
struct MagnitudeVector3d
{
private MagnitudeVector3d()
{
this.X = 2;
}
public double X, Y, Z;
public double Magnitude = 1;
}
var m = new MagnitudeVector3d(); // WARNING: no public ctor; treating this as default(MagnitudeVector3d)
Console.Write(m.X/Y/Z/Magnitude); // writes 0
void Foo<T>() where T : struct
{
var t = new T(); // warning: presence of parameterless public ctor is not known, therefore this is treated as default(T); use that instead
}
Foo<MagnitudeVector3d>(); // OK
void Bar<T>() where T : new()
{
var t = new T(); // OK
}
Bar<MagnitudeVector3d>(); // WARNING: no public ctor; generic new MagnitudeVector3d() will be treated as default(MagnitudeVector3d) |
Beta Was this translation helpful? Give feedback.
-
#5737 will be available in preview in 17.3 and alleviates some of the friction points here. |
Beta Was this translation helpful? Give feedback.
-
This discussion is extracted out from the (sometimes intense) discussion that occurred on the following issue: #5552
The crux of the problem is that there is a bit of an ergonomic cliff you fall off of when you transition from writing structs like so:
versus something like:
Due to the change the LDM decided on https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-03.md#parameterless-struct-constructors-revisited, the latter is no longer legal, and the user must provide a real constructor to initialize the struct. We want the user to provide a constructor explicitly as we do not want to synthesize an implicit no-arg constructor here that would then disappear if they ever added a has-arg constructor.
That said, for the user, adding this constructor is not pleasant. They end up having to write things like:
Both of these approaches are necessary so that the struct is definitely-assigned (DA) on constructor exit.
Note that this friction is a particular problem for the no-arg constructor. For any with-arg constructor, we always supported the user writing things like the following:
In other words, you could always chain the with-arg constructor to teh no-arg constructor. A constructor always leaves the struct def assigned, so this always worked as the chained constructor init'ed everything to zero, and then the actual constructor logic could run after that.
However, there's no equivalent for the no-arg constructor if the user provides it. the user cannot write:
As this would be a circular reference. They also can't do:
As this would blow away the initialized for
Magnitude
, defeating the purpose of having initializers in the first place.--
There are things we could do here though to make things more palatable for the user (with varying levels of pros/cons).
Options:
default
on exit leaving the struct in a DA state.public MagnitudeVector3d() { }
and get reasonable behavior.public MagnitudeVector3d() { }
and get reasonable behavior.public MagnitudeVector3d() : default {}
public record struct Foo(double X, double Y) : default { }
is totally sufficient for taht.Beta Was this translation helpful? Give feedback.
All reactions