属性(C# 编程指南)

属性是一种成员,它提供灵活的机制来读取、写入或计算数据字段的值。 属性显示为公共数据成员,但它们作为名为访问器的特殊方法来实现。 此功能使调用方能够轻松访问数据,还有助于提高数据的安全性和灵活性。 属性语法是字段的自然延伸。 字段定义存储位置:

public class Person
{
    public string? FirstName;

    // Omitted for brevity.
}

自动实现的属性

属性定义包含 getset 访问器的声明,这两个访问器用于检索该属性的值以及对其赋值:

public class Person
{
    public string? FirstName { get; set; }

    // Omitted for brevity.
}

上面的示例演示一个自动实现的属性。 此编译器会为该属性生成一个隐藏的支持字段。 编译器还实现 getset 访问器的正文。 任何属性都会应用于自动实现的属性。 你可以通过在属性上指定 field: 标记,将属性应用于编译器生成的支持字段。

你可以通过在属性的右大括号后面设置一个值,将属性初始化为默认值以外的其他值。 对于 FirstName 属性的的初始值,你可能更希望设置为空字符串而非 null。 你可以指定该空字符串,如以下代码所示:

public class Person
{
    public string FirstName { get; set; } = string.Empty;

    // Omitted for brevity.
}

访问控制

前面的示例显示了读/写属性。 你还可以创建只读属性,或者对 set 和 get 访问器提供不同的可访问性。 假设 Person 类只能从该类的其他方法中启用 FirstName 属性值更改。 可以为 set 访问器提供 private 可访问性,而不是 public 可访问性:

public class Person
{
    public string? FirstName { get; private set; }

    // Omitted for brevity.
}

可以从任意代码读取 FirstName 属性,但只能从 Person 类中的代码对其赋值。

可以向 set 和 get 访问器添加任何严格访问修饰符。 单个访问器上的访问修饰符必须比属性的访问更严格。 上述代码是合法的,因为 FirstName 属性为 public,但 set 访问器为 private。 不能声明具有 public 访问器的 private 属性。 属性声明还可以声明为 protectedinternalprotected internal,甚至 private

set 访问器有两个特殊的访问修饰符:

  • set 访问器可以将 init 作为其访问修饰符。 只能从对象初始值设定项或类型的构造函数调用该 set 访问器。 它比 set 访问器上的 private 更严格。
  • 自动实现的属性可以声明 get 访问器,而无需 set 访问器。 在这种情况下,编译器只允许从类型的构造函数调用 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 修饰符配合合适,以强制进行适当的初始化。

必需的属性

前面的示例允许调用方使用默认构造函数创建 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 属性设置为 nulldefault 是有效的。 如果类型不可为 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 没有支持字段。 该属性每次都会计算它。

具有支持字段的属性

可以将计算属性和存储的私有字段混合起来,创建缓存的计算属性。 例如,更新 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;
        }
    }
}

此实现可正常工作,因为 FirstNameLastName 属性为只读属性。 用户可以更改其名字。 若要更新 FirstNameLastName 属性以允许使用 set 访问器,则需要使 fullName 的任何缓存值失效。 修改 FirstNameLastName 属性的 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 关键字用于定义由 setinit 访问器分配的值。
  • 属性可以是读-写属性(既有 get 访问器又有 set 访问器)、只读属性(有 get 访问器,但没有 set 访问器)或只写访问器(有 set 访问器,但没有 get 访问器)。 只写属性很少见。

C# 语言规范

有关详细信息,请参阅 C# 语言规范中的属性。 该语言规范是 C# 语法和用法的权威资料。

另请参阅