属性是一种成员,它提供灵活的机制来读取、写入或计算数据字段的值。 属性显示为公共数据成员,但它们作为名为访问器的特殊方法来实现。 此功能使调用方能够轻松访问数据,还有助于提高数据的安全性和灵活性。 属性语法是字段的自然延伸。 字段定义存储位置:
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 具有名为 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提供辅助功能,而不是internalpublic:
public class Person
{
public string? FirstName { get; private set; }
// Omitted for brevity.
}
可以从任意代码读取 FirstName 属性,但只能从 Person 类中的代码对其赋值。
可以向 set 和 get 访问器添加任何严格访问修饰符。 单个访问器上的访问修饰符必须比属性的访问更严格。 上述代码是合法的,因为 FirstName 属性为 public,但 set 访问器为 private。 不能声明具有 private 访问器的 public 属性。 属性声明还可以声明为 protected、internal、protected internal,甚至 private。
set 访问器有两个特殊的访问修饰符:
-
set访问器可以将init作为其访问修饰符。 只能从对象初始值设定项或类型的构造函数调用该set访问器。 它比private访问器上的set更严格。 - 自动实现的属性可以在不使用访问器的情况下
get声明set访问器。 在这种情况下,编译器只允许从类型的构造函数调用set访问器。 它比init访问器上的set访问器更严格。
请按照这种方式修改 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 的任何缓存值失效。 修改 set 和 FirstName 属性的 LastName 访问器,以便重新计算 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 实现声明不能是自动实现的属性。 自动实现的属性使用与声明分部属性声明相同的语法。
属性
属性是类或对象中的一种智能字段形式。 从对象外部,它们看起来像对象中的字段。 但是,属性可以通过丰富的 C# 功能来实现。 你可以提供验证、不同的可访问性、迟缓计算或方案所需的任何要求。
- 无需自定义访问器代码的简单属性可以作为表达式正文定义或 自动实现的属性实现。
- 属性允许类公开获取和设置值的公共方法,而隐藏实现或验证代码。
- get 属性访问器用于返回属性值,而 set 属性访问器用于分配新值。 init 属性访问器仅用于在对象构造过程中分配新值。 这些访问器可以具有不同的访问级别。 有关详细信息,请参阅限制访问器可访问性。
-
value 关键字用于定义由
set或init访问器分配的值。 - 属性可以是读-写属性(既有
get访问器又有set访问器)、只读属性(有get访问器,但没有set访问器)或只写访问器(有set访问器,但没有get访问器)。 只写属性很少见。
C# 语言规范
有关详细信息,请参阅 C# 语言规范中的属性。 该语言规范是 C# 语法和用法的权威资料。