the symptom, in your words

"A struct's Default => new() silently skips your all-optional constructor"

✓ verified on 26.07.22
lane Building UIposted

▸ SYMPTOM

A struct exposes a convenience default:

snippet
public struct Config
{
    public int Radius;
    public float Falloff;

    // "Every parameter has a default, so new() should use these, right?"
    public Config(int radius = 8, float falloff = 0.5f)
    {
        Radius = radius;
        Falloff = falloff;
    }

    public static Config Default => new();
}

Config.Default comes back with every field zeroed (Radius = 0, Falloff = 0) — none of the = 8 / = 0.5f defaults you wrote on the constructor. The code compiles clean, reads correctly in review, and nothing hints that the constructor was skipped.

▸ CAUSE

new() on a struct type always binds to the implicit parameterless constructor the runtime guarantees for every value type. It does not bind to a declared constructor, even when every one of that constructor's parameters has a default value and it could technically be called with no arguments. The declared Config(int radius = 8, float falloff = 0.5f) is never invoked, so Default returns default(Config): all fields zero or null.

The trap is that this looks exactly like a correct call. new() is valid syntax, the constructor is valid, and both compile — but they refer to two different constructors, and the parameterless one wins for new().

▸ FIX

Make the Default property pass every argument explicitly, so it binds to the declared constructor:

snippet
public static Config Default => new(radius: 8, falloff: 0.5f);

Or declare an explicit parameterless constructor that chains to the parameterized one, so new() routes through your defaults:

snippet
public Config() : this(radius: 8, falloff: 0.5f) { }

Then pin it with a test that asserts the populated field values, not merely that construction succeeded:

snippet
var c = Config.Default;
Assert.AreEqual(8, c.Radius);
Assert.AreEqual(0.5f, c.Falloff);

▸ WHY IT WORKS

Passing arguments explicitly (or chaining from an explicit parameterless constructor) forces overload resolution onto your declared constructor instead of the implicit value-type one, so the intended defaults actually run. Asserting the resulting field values — rather than "it constructed" — catches the silent-zero regression the instant it reappears, because the syntax alone will never reveal which constructor ran.

Verified on engine 26.07.22: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Published

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord