属性(C# 编程指南)
属性是一种成员,它提供灵活的机制来读取、写入或计算数据字段的值。 属性显示为公共数据成员,但它们作为名为访问器的特殊方法来实现。 此功能使调用方能够轻松访问数据,还有助于提高数据的安全性和灵活性。 属性语法是字段的自然延伸。 字段定义存储位置:
public class Person
{
public string? FirstName;
// Omitted for brevity.
}
自动实现的属性
属性定义包含 get
和 set
访问器的声明,这两个访问器用于检索该属性的值以及对其赋值:
public class Person
{
public string? FirstName { get; set; }
// Omitted for brevity.
}
前面的示例显示了自动 实现的属性。 此编译器会为该属性生成一个隐藏的支持字段。 编译器还实现 get
和 set
访问器的正文。 任何属性都应用于自动实现的属性。 你可以通过在属性上指定 field:
标记,将属性应用于编译器生成的支持字段。
你可以通过在属性的右大括号后面设置一个值,将属性初始化为默认值以外的其他值。 对于 FirstName
属性的的初始值,你可能更希望设置为空字符串而非 null
。 你可以指定该空字符串,如以下代码所示:
public class Person
{
public string FirstName { get; set; } = string.Empty;
// Omitted for brevity.
}
字段支持的属性
在 C# 13 中,可以使用关键字预览功能在访问器 field
中添加验证或其他逻辑。 关键字 field
访问属性的编译器合成后盾字段。 它使你可以编写属性访问器,而无需显式声明单独的后盾字段。
public class Person
{
public string? FirstName
{
get;
set => field = value.Trim();
}
// Omitted for brevity.
}
重要
关键字 field
是 C# 13 中的预览功能。 必须使用 .NET 9 并将元素preview
设置为<LangVersion>
项目文件中,才能使用field
上下文关键字。
应注意在 field
具有名为 field
字段的类中使用关键字功能。 新 field
关键字将隐藏属性访问器范围中命名 field
的字段。 可以更改变量的名称 field
,或使用 @
令牌将标识符引用 field
为 @field
。 可以通过阅读关键字的功能规范field
来了解详细信息。
必需的属性
前面的示例允许调用方使用默认构造函数创建 Person
,而无需设置 FirstName
属性。 属性已将类型更改为可以为 null 的字符串。 从 C# 11 开始,你可以要求调用方设置属性:
public class Person
{
public Person() { }
[SetsRequiredMembers]
public Person(string firstName) => FirstName = firstName;
public required string FirstName { get; init; }
// Omitted for brevity.
}
上述代码对 Person
类进行了两项更改。 首先,FirstName
属性声明包含了 required
修饰符。 这意味着任何创建新 Person
的代码都必须使用对象初始值设定项来设置此属性。 其次,采用 firstName
参数的构造函数具有 System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute 特性。 此特性通知编译器此构造函数设置了所有所有 required
成员。 使用此构造函数的调用方不需要使用对象初始值设定项来设置 required
属性。
重要
不要将 required
与不可为 null 混淆。 将 required
属性设置为 null
或 default
是有效的。 如果类型不可为 null,例如这些示例中的 string
,则编译器会发出警告。
var aPerson = new Person("John");
aPerson = new Person{ FirstName = "John"};
// Error CS9035: Required member `Person.FirstName` must be set:
//aPerson2 = new Person();
表达式主体定义
属性访问器通常由单行语句组成。 访问器将分配或返回表达式的结果。 可以将这些属性作为 expression-bodied 成员来实现。 =>
令牌后跟用于为属性赋值或从属性中检索值的表达式,即组成了表达式主体定义。
只读属性可以将 get
访问器作为 expression-bodied 成员实现。 下面的示例将只读 Name
属性作为 expression-bodied 成员实现:
public class Person
{
public Person() { }
[SetsRequiredMembers]
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public required string FirstName { get; init; }
public required string LastName { get; init; }
public string Name => $"{FirstName} {LastName}";
// Omitted for brevity.
}
Name
属性为计算属性。 Name
没有支持字段。 该属性每次都会计算它。
访问控制
前面的示例显示了读/写属性。 你还可以创建只读属性,或者对 set 和 get 访问器提供不同的可访问性。 假设类 Person
应仅启用从类中的其他方法更改属性的值 FirstName
。 你可以为 set 访问器private
提供辅助功能,而不是internal
public
:
public class Person
{
public string? FirstName { get; private set; }
// Omitted for brevity.
}
可以从任意代码读取 FirstName
属性,但只能从 Person
类中的代码对其赋值。
可以向 set 和 get 访问器添加任何严格访问修饰符。 单个访问器上的访问修饰符必须比属性的访问更严格。 上述代码是合法的,因为 FirstName
属性为 public
,但 set 访问器为 private
。 不能声明具有 public
访问器的 private
属性。 属性声明还可以声明为 protected
、internal
、protected internal
,甚至 private
。
set
访问器有两个特殊的访问修饰符:
set
访问器可以将init
作为其访问修饰符。 只能从对象初始值设定项或类型的构造函数调用该set
访问器。 它比set
访问器上的private
更严格。- 自动实现的属性可以在不使用访问器的情况下
set
声明get
访问器。 在这种情况下,编译器只允许从类型的构造函数调用set
访问器。 它比set
访问器上的init
访问器更严格。
请按照这种方式修改 Person
类,如下所示:
public class Person
{
public Person(string firstName) => FirstName = firstName;
public string FirstName { get; }
// Omitted for brevity.
}
前面的示例要求调用方使用包含 FirstName
参数的构造函数。 调用方无法使用对象初始值设定项向属性分配值。 若要支持初始值设定项,可以将 set
访问器设置为 init
访问器,如以下代码所示:
public class Person
{
public Person() { }
public Person(string firstName) => FirstName = firstName;
public string? FirstName { get; init; }
// Omitted for brevity.
}
这些修饰符通常与 required
修饰符配合合适,以强制进行适当的初始化。
具有支持字段的属性
可以将计算属性和存储的私有字段混合起来,创建缓存的计算属性。 例如,更新 FullName
属性,以便在第一次访问时进行字符串格式设置:
public class Person
{
public Person() { }
[SetsRequiredMembers]
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public required string FirstName { get; init; }
public required string LastName { get; init; }
private string? _fullName;
public string FullName
{
get
{
if (_fullName is null)
_fullName = $"{FirstName} {LastName}";
return _fullName;
}
}
}
此实现可正常工作,因为 FirstName
和 LastName
属性为只读属性。 用户可以更改其名字。 若要更新 FirstName
和 LastName
属性以允许使用 set
访问器,则需要使 fullName
的任何缓存值失效。 修改 FirstName
和 LastName
属性的 set
访问器,以便重新计算 fullName
字段:
public class Person
{
private string? _firstName;
public string? FirstName
{
get => _firstName;
set
{
_firstName = value;
_fullName = null;
}
}
private string? _lastName;
public string? LastName
{
get => _lastName;
set
{
_lastName = value;
_fullName = null;
}
}
private string? _fullName;
public string FullName
{
get
{
if (_fullName is null)
_fullName = $"{FirstName} {LastName}";
return _fullName;
}
}
}
此最终版本仅在必要时计算 FullName
属性。 如果以前计算的版本有效,则使用它。 否则,计算会更新缓存的值。 使用此类的开发人员无需了解实现的细枝末节。 这些内部更改不会影响 Person 对象的使用。
从 C# 13 开始,你可以在 partial
类中创建 partial
属性。 属性的 partial
实现声明不能是自动实现的属性。 自动实现的属性使用与声明分部属性声明相同的语法。
属性
属性是类或对象中的一种智能字段形式。 从对象外部,它们看起来像对象中的字段。 但是,属性可以通过丰富的 C# 功能来实现。 你可以提供验证、不同的可访问性、迟缓计算或方案所需的任何要求。
- 无需自定义访问器代码的简单属性可以作为表达式正文定义或 自动实现的属性实现。
- 属性允许类公开获取和设置值的公共方法,而隐藏实现或验证代码。
- get 属性访问器用于返回属性值,而 set 属性访问器用于分配新值。 init 属性访问器仅用于在对象构造过程中分配新值。 这些访问器可以具有不同的访问级别。 有关详细信息,请参阅限制访问器可访问性。
- value 关键字用于定义由
set
或init
访问器分配的值。 - 属性可以是读-写属性(既有
get
访问器又有set
访问器)、只读属性(有get
访问器,但没有set
访问器)或只写访问器(有set
访问器,但没有get
访问器)。 只写属性很少见。
C# 语言规范
有关详细信息,请参阅 C# 语言规范中的属性。 该语言规范是 C# 语法和用法的权威资料。