.NET 10 LTS(2025.11)· C# 13 预览 → C# 14 正式 · 最受期待的特性
field 关键字正式版。这是 .NET 项目中最常见的代码模式之一——属性有逻辑,需要手动声明后备字段:
// 你写了无数次的三行样板:
private string _name = ""; // ① 声明后备字段
public string Name
{
get => _name; // ② 写 get
set => _name = value?.Trim() ?? ""; // ③ 写 set,引用 _name
}
问题不在行数——在于这四个字符的语义冗余:IDE 帮你生成 _name,你手动维护它与属性名的对应关系,改属性名时还要记得改字段名。属性数量一多,类声明区被几十个后备字段淹没。
private string _name; 要向上或向下翻找对应的属性,确认"这个字段确实只被一个属性使用"。field 消除了这层心理开销。
// C# 14:后备字段完全消失
public string Name
{
get; // 编译器生成 get
set => field = value?.Trim() ?? ""; // field = 编译器合成的后备字段
}
field 是上下文关键字——只在属性访问器内有特殊含义。在类中其他地方,field 仍可作为普通标识符使用。
private int _age;
public int Age
{
get => _age;
set => _age = value >= 0
? value
: throw new ArgumentOutOfRangeException();
}
private string _email;
public string Email
{
get => _email;
set => _email = value?.Contains('@') == true
? value
: throw new ArgumentException();
}
// 后备字段完全不可见——编译器代劳
public int Age
{
get;
set => field = value >= 0
? value
: throw new ArgumentOutOfRangeException();
}
public string Email
{
get;
set => field = value?.Contains('@') == true
? value
: throw new ArgumentException();
}
| 规则 | 说明 |
|---|---|
| 作用域 | field 仅在属性 get 或 set 访问器体内有效 |
| 类型 | field 的类型与属性类型相同 |
| 至少一个访问器有体 | 必须为 get 或 set(或两者)提供 body。两个都是 ; → 普通自动属性,不需要 field |
| 另一个可自动 | 可以给 set 写 body,让 get; 保持自动——编译器补全另一半 |
| 上下文关键字 | 在属性外,field 仍可作为普通标识符 |
| 不适用于索引器 | field 只用于属性,不用于 this[...] 索引器 |
// 模式 1:get 自动 + set 自定义(最常见)
public string Name { get; set => field = value?.Trim() ?? ""; }
// 模式 2:get 自定义 + set 自动
public string Display { get => field ?? "未知"; set; }
// 模式 3:两者都自定义
public int Count
{
get => field;
set => field = Math.Max(0, value);
}
// 模式 4:属性初始器 + field
public List<string> Items { get; set => field = value; } = new(); // 初始器照常工作
如果你的类中已经有一个名为 field 的成员(字段、属性、方法),在属性访问器内编译器默认把 field 当作合成的后备字段。要访问你自己的 field 成员,使用 @field 或 this.field:
public class Example
{
private string field; // 你自己声明的字段(不推荐这样命名,但可能存在于旧代码)
public string Name
{
get;
set
{
field = value; // ← 合成的后备字段(属性类型 string)
@field = value; // ← 显式声明的 field 字段(也是 string,巧合)
this.field = value; // ← 同上,用 this. 消除歧义
}
}
}
field。这从来不是一个好名字——它不传达任何语义。如果旧代码中有,趁机重命名。
这是用户最关心的部分。编译器将 field 属性降低为带有编译器生成后备字段的普通属性。以下是 SharpLab 验证的实际行为:
// ──── 你写的代码 ────
public string Message
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
// ──── 编译器降低后的等效代码 ────
[CompilerGenerated]
private string <Message>k__BackingField; // ← 编译器生成的字段名
public string Message
{
get => <Message>k__BackingField;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
<Message>k__BackingField = value;
}
}
<PropertyName>k__BackingField——与普通自动属性完全相同的命名模式。这意味着 field 关键字不引入任何新的运行时概念——它纯粹是语法糖,编译后的 IL 与手动写后备字段完全一致。
因为 field 在编译时完全消除,IL 与手动写法完全相同。不存在任何额外的方法调用、装箱、或运行时查找。
// field 属性生成的 IL:
// .field private string '<Message>k__BackingField'
// get_Message: ldarg.0; ldfld <Message>k__BackingField; ret
// set_Message: ldarg.1; brtrue.s OK; ldstr "..."; newobj ArgumentNullException; throw
// OK: ldarg.0; ldarg.1; stfld <Message>k__BackingField; ret
//
// BINARY IDENTICAL to the manual _message version.
MVVM 和 Blazor 组件中最常见的模式——属性变化时既要验证又要通知 UI:
public class UserViewModel : INotifyPropertyChanged
{
// C# 13 写法:6 行样板(字段声明 + 两个访问器)
// C# 14:后备字段消失
public string UserName
{
get;
set
{
if (field == value) return; // field 是旧值
field = value?.Trim() ?? ""; // 验证 + 赋值
OnPropertyChanged(); // 通知 UI
}
}
public event PropertyChangedEventHandler? PropertyChanged;
void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new(name));
}
if (field == value) return;)——省掉一次 PropertyChanged 通知。
public class OrderService
{
// 懒加载:get 有逻辑,set 保持自动
public List<Order> RecentOrders
{
get
{
if (field == null)
field = LoadFromDatabase();
return field;
}
set; // set 保持自动——外部可以直接赋缓存值
}
private List<Order> LoadFromDatabase() { /* ... */ }
}
public class Sensor
{
private readonly ILogger _log;
public double Temperature
{
get;
set
{
if (value < -50 || value > 150)
throw new ArgumentOutOfRangeException();
_log.LogInformation("温度 {Old} → {New}", field, value);
field = value;
}
}
}
// field 与 C# 9 init 配合
public class Person
{
public required string Name { get; init => field = value?.Trim() ?? ""; }
public int Age { get; init => field = Math.Clamp(value, 0, 150); }
}
// 使用:
var p = new Person { Name = " Alice ", Age = 30 };
// Name = "Alice"(已 trim),Age = 30(在范围内)
init + field 是最佳拍档。before: init 需要完整属性体+手动后备字段,样板量最大。after: 一行写完带验证的不可变属性。
public string Name
{
get;
set => field = value; // ✅ 在 set 内
}
public void Reset()
{
field = ""; // ❌ CS0103: 当前上下文中不存在名称 "field"
Name = ""; // ✅ 通过属性赋值——这才是正确的
}
// 这不是 field 属性——是普通自动属性
public string Simple { get; set; } // get;set; 都无体 → 不需要 field
// 至少要有一个访问器写 body 才算 field 属性:
public string Validated { get; set => field = value ?? ""; } // ✅ set 有 body
public string LazyGet { get => field ??= Load(); set; } // ✅ get 有 body
public int? Age
{
get;
set => field = value; // field 的类型是 int?——与属性相同
}
// 你不能让 field 类型与属性不同——这是设计意图:
// 属性 = 对单个后备字段的受控访问。
// 如果需要不同类型转换,仍用手动字段。
public string Name => _name; // 表达式体属性——没有后备字段
public string Name { get; set => ... } // field 属性——有后备字段
// 两者是不同的东西。field 属性一定有编译器生成的后备字段。
C# 1.0 起
private string _x;
public string X
{
get => _x;
set => _x = value;
}
✅ 完全控制
✅ 可改变字段类型
❌ 声明样板
❌ 改名时手动同步
本课
public string X
{
get;
set => field = value;
}
✅ 零样板
✅ 编译器维护
✅ IL 完全等价
❌ 一个访问器须自动
❌ 不能改字段类型
源生成器方案
// 声明:
public partial string X
{ get; set; }
// 另一文件实现
✅ 声明/实现分离
✅ 源生成器友好
❌ 两个文件
❌ 工具链复杂
选择指南:日常带逻辑的属性 → field。需要非标准后备存储 → 手动字段。源生成器生成的属性 → partial 属性。
field 关键字在编译后产生什么额外运行时开销?
以下代码执行后,c.field 的值最终是什么?(注意:x 不是变量名,看注释)
public class C {
private string field = "A";
public string P {
get;
set { field = "B"; this.field = "C"; }
}
}
var c = new C();
c.P = "X";
// Console.WriteLine(c.field); — 输出?
对于 public string Name { get; set => field = value; },编译器生成的字段名是什么模式?
以下哪个场景不适合使用 field 关键字?
field 关键字在哪个版本首次作为预览特性出现,又在哪个版本正式发布?
field 是 C# 14 的"开胃菜"——它消除的是你每天都能感受到的摩擦。但 C# 14 最大的新特性是扩展成员:不仅是扩展方法,还有扩展属性、静态扩展、扩展运算符——这是 C# 3 引入扩展方法以来对扩展机制的最大升级。
📖 NRT 速查 · 📖 Records 速查 · ← L20: C# 13 小特性 · L22: C# 14 扩展成员 →