"A struct's Default => new() silently skips your all-optional constructor"
▸ SYMPTOM
A struct exposes a convenience default:
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:
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:
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:
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.
- Published