


















You need a property that validates its input. In C# 13 and earlier, that means writing a private backing field, a get accessor that returns it, and a set accessor that validates the value before storing it. Three moving parts for one property:
Csharp
public class Greeting
{
private string _msg = "Hello";
public string Message
{
get => _msg;
set => _msg = value ?? throw new ArgumentNullException(nameof(value));
}
} One property, one validation rule, three lines of ceremony. The backing field _msg exists only to give set something to write to and get something to read from. It carries no meaning of its own.
Multiply this across a configuration class with five or six validated properties, and the noise adds up fast.
C# 14 introduces field, a contextual keyword you can use inside property accessors to reference the compiler-generated backing field. You write the accessor that needs custom logic. The compiler generates the other one for you, exactly like an auto-property:
Csharp
public class Greeting
{
public string Message
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
} Same validation. No explicit backing field. The get accessor is auto-implemented by the compiler because it has no body, and field in the set accessor points to the compiler-synthesized backing field behind the scenes.
field is a contextual keyword. It only has special meaning inside a property accessor body. Outside of accessors, field is still a regular identifier. field in get, set, or init accessors. Provide a body for one, both, or just the one that needs logic. field with a property initializer: public string Name { get; set => field = value.Trim(); } = "Default"; Configuration classes are full of properties that need guard clauses. The field keyword cuts the noise:
Csharp
public class OAuthClientOptions
{
public string ClientId
{
get;
set => field = !string.IsNullOrWhiteSpace(value)
? value
: throw new ArgumentException("ClientId cannot be empty.", nameof(value));
}
public Uri RedirectUri
{
get;
set => field = value.IsAbsoluteUri
? value
: throw new ArgumentException("RedirectUri must be an absolute URI.", nameof(value));
}
} Each property enforces its contract in the setter. No backing fields cluttering the class. The get accessors are auto-generated.
The field ??= pattern gives you lazy initialization without Lazy<T> or an explicit backing field:
Csharp
public class ExpensiveService
{
public Connection DatabaseConnection
{
get => field ??= CreateConnection();
}
private Connection CreateConnection()
{
// Imagine this is expensive
return new Connection("Server=localhost;Database=app");
}
}
public class Connection(string connectionString)
{
public string ConnectionString => connectionString;
} The first time you read DatabaseConnection, it calls CreateConnection() and stores the result. Every subsequent read returns the cached instance. This works because field starts as null (the default for reference types), and ??= only assigns when the left side is null.
One caveat: this is not thread-safe. If multiple threads hit the getter simultaneously, CreateConnection() could run more than once. For thread-safe lazy initialization, stick with Lazy<T>.
A simple pattern for cleaning up string input:
Csharp
public class Person
{
public string? FirstName
{
get;
set => field = value?.Trim();
}
} Every assignment to FirstName gets trimmed. The caller never needs to remember to trim, and the getter returns the cleaned value without any transformation.
ViewModels in MVVM apps share the same boilerplate: check whether the value has changed, update the field, and raise the event. The field keyword helps:
Csharp
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class SettingsViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string Theme
{
get;
set
{
if (field != value)
{
field = value;
OnPropertyChanged();
}
}
} = "Light";
public int FontSize
{
get;
set
{
if (field != value)
{
field = value;
OnPropertyChanged();
}
}
} = 14;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
} Compare this with the version that declares _theme and _fontSize fields manually. The field keyword removes two fields and two get accessor bodies. The logic stays, the scaffolding goes.
Because field is a contextual keyword, it only has special meaning inside a property accessor. But if your class already has a member or local variable named field, you need to disambiguate:
Csharp
public class Sensor
{
private string field; // a member named "field" — not ideal, but it happens
public string Reading
{
get;
set
{
// "field" here refers to the compiler-generated backing field
field = value;
// Use this.field to access the instance member named "field"
this.field = value;
}
}
public string Label
{
get;
set
{
// Use @field to refer to a local variable named "field"
var @field = value.ToUpper();
field = @field; // assigns the keyword backing field from the local
}
}
}The disambiguation rules:
| Syntax | Refers to |
|---|---|
| field (inside accessor) | Compiler-synthesized backing field |
| this.field | Instance member named field |
| @field | Local variable or parameter named field |
The simplest fix: rename the member. If you have a member called field, this is a good time to give it a more descriptive name.
A few things to know before you use field everywhere:
field in regular method bodies, constructors, or anywhere outside a property accessor. It's scoped to get, set, and init. [field: NonSerialized] attribute target on a field-keyword property, just like with auto-properties. But property-level attributes like [JsonIgnore] affect the property, not the backing field. If you need an attribute that targets the backing field and no field: target exists for it, you need an explicit backing field. The options pattern is everywhere in ASP.NET Core. Classes that configure middleware, authentication handlers, and token validation all benefit from validated properties. Here's what a token validation options class looks like with field:
Csharp
public class TokenValidationOptions
{
public required string Issuer
{
get;
set => field = !string.IsNullOrWhiteSpace(value)
? value
: throw new ArgumentException("Issuer cannot be empty.", nameof(value));
}
public required string Audience
{
get;
set => field = !string.IsNullOrWhiteSpace(value)
? value
: throw new ArgumentException("Audience cannot be empty.", nameof(value));
}
public TimeSpan ClockSkew
{
get;
set => field = value >= TimeSpan.Zero
? value
: throw new ArgumentOutOfRangeException(nameof(value), "ClockSkew must not be negative.");
} = TimeSpan.FromMinutes(5);
public int MaxTokenLifetimeMinutes
{
get;
set => field = value > 0
? value
: throw new ArgumentOutOfRangeException(nameof(value), "MaxTokenLifetimeMinutes must be positive.");
} = 60;
} Four validated properties. Zero backing fields. Issuer and Audience are required, so the compiler enforces that callers set them at construction. ClockSkew and MaxTokenLifetimeMinutes have sensible defaults via initializers. Every property enforces its contract at the point of assignment, and any invalid value throws immediately rather than being silently accepted and failing later at runtime.
Configuration classes with validation are everywhere in identity code. Duende IdentityServer uses the options pattern for configuring token lifetimes, issuer URIs, signing credentials, and more. The field keyword makes these classes cleaner without sacrificing the validation that keeps configuration errors from reaching production.
The field keyword solves a narrow problem well: adding logic to a property without the ceremony of an explicit backing field. It won't change how you architect applications, but it will make the properties you write every day a little cleaner. Start using it anywhere you have a backing field that exists only to support a get/set pair.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。