DEV Community

Sukhpinder Singh
Sukhpinder Singh

Posted on Edited on

The Backing Field a Typo Could Reach

A discount came back as 110% in a test last week and I spent a solid ten minutes insisting that was impossible. I had a setter. The setter clamped. Math.Clamp(value, 0, 100), sitting right there, no way past it. Except there was a way past it, and I'd built the door myself.

Here's the property, more or less how it looked:

sealed class DiscountOld
{
    private int _percent;

    public int Percent
    {
        get => _percent;
        set => _percent = Math.Clamp(value, 0, 100);
    }
}
Enter fullscreen mode Exit fullscreen mode

Nothing wrong with that. Assign Percent = 150 and you get 100 back. I'd tested exactly that case and moved on. The setter does its job every single time it runs. That last bit is the part I didn't think hard enough about.

The line I wrote three weeks later

A seasonal promo needed a flat bump, and I added a method to the same class to do it. Past me, in a hurry, reached for the field because it was shorter and it was right there:

public void ApplySeasonalBump()
{
    _percent += 90;   // this never asks the setter for permission
}
Enter fullscreen mode Exit fullscreen mode

So a row that started at a sensible 20 goes to 110, and the clamp never gets a vote, because _percent += 90 touches the backing field and the backing field doesn't clamp anything. The setter was never the guard I thought it was. It was a guard on one specific door, and I'd left a second door open the day I gave the field a name.

Running both side by side makes it plain:

DiscountOld.Percent  = 110
Enter fullscreen mode Exit fullscreen mode

The clamp is still there. It just didn't run on the path that mattered.

The field keyword removes the second door

C# 14 (the field contextual keyword, stable now in .NET 10) lets a property refer to its own backing store without you ever naming it:

sealed class DiscountNew
{
    public int Percent
    {
        get => field;
        set => field = Math.Clamp(value, 0, 100);
    }

    public void ApplySeasonalBump()
    {
        Percent += 90;   // the only door left is the property
    }
}
Enter fullscreen mode Exit fullscreen mode

The interesting word there is nameless. The compiler still generates a backing field — same as before, nothing clever happening underneath. But there's no identifier for it anywhere in your source. So when I go to write the bump method, I can't reach for _percent, because _percent doesn't exist. My hand is forced onto Percent, which runs the setter, which clamps:

DiscountNew.Percent  = 100
Enter fullscreen mode Exit fullscreen mode

And if you don't believe the field is truly unreachable, try it. Drop _percent += 90 into the second class and the build stops cold:

error CS0103: The name '_percent' does not exist in the current context
Enter fullscreen mode Exit fullscreen mode

That error is the whole point. The bug from the top of this post isn't fixed here so much as it's made unspellable.

This isn't a speed thing, and I want to be honest about that

I write a lot of posts where the payoff is a number on a stopwatch. This isn't one of them. The field keyword compiles down to the same backing field you'd have written by hand, so there's no allocation saved, no cycle shaved, nothing to benchmark. If you go looking for a performance win here you'll come back empty, and that's fine. The win is a different currency: it's one fewer category of bug that a tired colleague can introduce at 5pm. I'll take that trade over a microsecond most days.

My honest opinion, for whatever it's worth: the sweet spot is any property that does real work in its setter — clamping, trimming, normalizing, raising a changed event. Those are exactly the properties where skipping the setter is a bug, and field makes skipping it impossible. For a plain get; set; with no logic, it buys you nothing, so I don't bother.

Where it bites

One thing to watch, and it's the reason this wasn't just quietly turned on for everyone. If you already have something named field in scope, the keyword and the name collide. A local called field inside an accessor is now a hard error:

error CS9273: In language version 14.0, 'field' is a keyword within a property
accessor. Rename the variable or use the identifier '@field' instead.
Enter fullscreen mode Exit fullscreen mode

And a member named field gets you a warning that the keyword is quietly binding to the synthesized backing store instead of your member (warning CS9258), with @field or this.field as the escape hatch. Not common names, but if you've got a domain where "field" is a real noun — forms, databases, farming apps — grep for it before you bump your language version. I'd rather know about the collision from a grep than from a warning I scrolled past.

Full runnable sample, both classes and the output above, is the two snippets in this post — drop them in a net10.0 console project with <LangVersion>14.0</LangVersion> and run it.

Have you found a good use for field yet, or hit a collision you didn't expect? Curious which camp people land in.

— still clamping things that were already clamped, apparently

Top comments (0)