A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://github.com/dotnet/csharplang/discussions/5635 below:

Field initializers in structs can often lead to friction points for authors · dotnet/csharplang · Discussion #5635 · GitHub

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:

struct Vector3d
{
    double X, Y, Z;
}

var v = new Vector3d();

versus something like:

struct MagnitudeVector3d
{
    double X, Y, Z;
    double Magnitude = 1;
}

var m = new MagnitudeVector3d();

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:

struct MagnitudeVector3d
{
    double X, Y, Z;
    double Magnitude = 1;

    public MagnitudeVector3d()
    {
        X = 0;
        Y = 0;
        Z = 0;
    }
}

// or something like:

struct MagnitudeVector3d
{
    double X = 0, Y = 0, Z = 0;
    double Magnitude = 1;

    public MagnitudeVector3d()
    {
    }
}

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:

struct Vector3d
{
    double X, Y, Z;

    public Vector3d(double X) : this() { /*... */ }
}

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:

struct MagnitudeVector3d
{
    double X = 0, Y = 0, Z = 0;
    double Magnitude = 1;

    public MagnitudeVector3d() : this()
    {
    }
}

As this would be a circular reference. They also can't do:

struct MagnitudeVector3d
{
    double X = 0, Y = 0, Z = 0;
    double Magnitude = 1;

    public MagnitudeVector3d()
    {
        this = default;
    }
}

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:

  1. Consider changing how DA works in these cases. For example, if any field was "not-assigned-along-any-path-on-exit" (insert handwaving here on how to precisely state that), then it could be assigned to default on exit leaving the struct in a DA state.
    1. Pros, it would allow the user to just write public MagnitudeVector3d() { } and get reasonable behavior.
    2. Cons, it touches DA and coudl be hugely complex to specify this precisely (and implement).
  2. We could always have all constructors initobj the struct at the start.
    1. Pros, it would allow the user to just write public MagnitudeVector3d() { } and get reasonable behavior.
    2. Cons, it might really add a perf cost because of the redundant initialization (though potentially we could detect and emit good code with high complexity to only init what is necessary).
  3. We could introduce a whole new syntax to let the user opt into this. For example: public MagnitudeVector3d() : default {}
    1. Pros, it lets the user just write that simple form, and not have to care about individual assignment.
    2. Cons, it's a full new feature with new syntax. We'd also have to figure out how this would work with primary constructors with record-structs. Though perhaps public record struct Foo(double X, double Y) : default { } is totally sufficient for taht.
  4. we could have a generator always synthesize this explicit no-arg constructor (if not already there). it would assign any fields without initializers. Other constructors could then chain to this.
    1. Pros, nothing for the user to do (except maybe add an attribute).
    2. Cons, Need to create and support this analyzer. Potential user confusion about what was going on here.

RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4