属性(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.
}

字段支持的属性

在 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 属性设置为 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 没有支持字段。 该属性每次都会计算它。

访问控制

前面的示例显示了读/写属性。 你还可以创建只读属性,或者对 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。 不能声明具有 public 访问器的 private 属性。 属性声明还可以声明为 protectedinternalprotected 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;
        }
    }
}

此实现可正常工作,因为 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# 语法和用法的权威资料。

另请参阅