类属性和访问器入门

已完成

类属性提供一种灵活的机制来读取、写入或计算数据字段的值。 它们表现为公共数据成员,但实际上是通过被称为访问器的特殊方法实现的。 此功能使调用方能够轻松访问数据,并且仍有助于提升数据安全性和灵活性。

使用属性

属性结合了字段和方法的多个方面。 对于对象的使用者来说,属性看起来像是一个字段。 访问属性需要与访问字段相同的语法。 对于类的实现者,属性是一个或两个代码块,表示 get 访问器和/或 set 或 init 访问器。 读取属性时,将执行 get 访问器的代码块。 给属性赋值时,将执行 set 或 init 访问器的代码块。 没有 set 访问器的属性被视为只读属性。 将不带 get 访问器的属性视为只写。 将具有以上两个访问器的属性视为读写。 可以使用 init 访问器而不是 set 访问器来启用属性设置为对象初始化的一部分,否则使其成为只读属性。

与字段不同,属性不会分类为变量。 因此,不能将属性作为 ref 或 out 参数传递。

属性通常用于支持以下方案:

  • 他们可以在允许更改之前验证数据。
  • 它们可以在类上透明地暴露数据,而这些数据是从其他来源(例如数据库)检索得到的。
  • 当数据发生更改(例如引发事件或更改其他字段的值)时,它们可以采取措施。

属性在类定义的代码块中声明。 属性的声明方式是先指定字段的访问级别,再指定属性的类型,再指定属性的名称,最后指定一个用于声明 get 访问器和/或 set 访问器的代码块。

get 访问器

get 访问器的正文类似于方法。 它必须返回属性类型的值。 C# 编译器和实时 (JIT) 编译器检测实现 get 访问器的常见模式,并针对这些模式进行优化。

例如,一个不执行任何计算、仅返回某个字段的 get 访问器很可能会被优化为对该字段的内存读取。 自动实现的属性将在本模块的下一单元中介绍,它们遵循这种模式,并受益于这些优化。 但是,虚拟 get 访问器方法无法内联,因为编译器在编译时不知道在运行时可能实际调用哪种方法。

以下示例演示一个 get 访问器,该访问器返回专用字段 _name的值:


public class Employee
{
    private string _name = "unknown";  // the name field
    public string Name
    {
        get { return _name; }  // the Name property
    }
}

在此示例中,Employee 类包括专用字段和公共属性。 下面是 Employee 类定义的说明:

  • Employee 类是使用公共访问修饰符定义的,这意味着可以从同一程序集或其他引用它的程序集中的其他任何代码进行访问。
  • 专用字段 _name 用 string 类型声明,并分配默认值 "unknown"。
  • 公共属性 Name 使用 string 类型声明。 该属性包括一个 get 访问器,该访问器返回 _name 字段的值。

注意

为属性访问器提供值的字段通常称为 后盾字段。

花一分钟时间考虑实例化 Employee 对象的代码,并读取 Name 属性的值:

Employee employee = new Employee();  // create an instance of the Employee class

Console.Write(employee.Name);  // use the get accessor to read the value of the Name property

此代码示例实例化一个名为 employee 的新 Employee 对象。 当代码引用 employee.Name 属性时,除非该属性是赋值的目标,否则将调用 get 访问器来读取该属性的值。 在这种情况下,get 访问器返回 _name 字段的值,该字段被赋值为 "unknown"。

get 访问器的代码块也可用于返回计算值。 如果要确保属性始终返回非 null 值,这非常有用。 例如:


public class Manager
{
    private string? _name;
    public string Name
    {
        get
        {
            return _name != null ? _name : "NA";
        }
    }
}

set 访问器

set 访问器类似于返回类型为 void 的方法。 它使用名为 value的隐式参数,其类型为属性的类型。 编译器和 JIT 编译器还识别 set 或 init 访问器的常见模式。 这些常见模式已被优化,会直接写入后备字段所在的内存。 在以下示例中,向 Name 属性添加了 set 访问器:


class Student
{
    private string? _name;  // the name field
    public string Name    // the Name property
    {
        get
        {
            return _name != null ? _name : "NA";
        }
        set
        {
            _name = value;
        }
    }
}

花点时间考虑创建 Student 类实例的代码。 将值分配给 Name 属性时,将使用提供新值的参数调用 set 访问器。 例如:


var student = new Student();
student.Name = "StudentName";  // the set accessor is invoked here

Console.Write(student.Name);  // the get accessor is invoked here

init 访问器

创建 init 访问器的代码与创建 set 访问器的代码相同,只不过使用 init 关键字而不是 set。 区别在于, init 访问器只能在构造函数中使用,也可以使用 对象初始值设定项。

声明和使用读写属性

属性提供了公有数据成员的便利,同时避免了对对象数据进行未受保护、未受控制且未经验证的访问所带来的风险。 属性声明访问器:分配和检索基础数据成员中的值的特殊方法。 set 访问器允许分配数据成员,get 访问器检索数据成员值。

此示例显示了一个 Person 类,该类具有两个属性:Name(string)和 Age(int)。 这两个属性都提供 get 和 set 访问器,因此它们被视为读/写属性。


class Person
{
    private string _name = "N/A";
    private int _age = 0;

    // Declare a Name property of type string:
    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value;
        }
    }

    // Declare an Age property of type int:
    public int Age
    {
        get
        {
            return _age;
        }
        set
        {
            _age = value;
        }
    }
}

class TestPerson
{
    static void Main()
    {
        // Create a new Person object named person:
        Person person = new Person();

        // Print out the default name and age of the person:
        Console.WriteLine($"Person details - Name = {person.Name}, Age = {person.Age}");

        // Set some values on the person object:
        person.Name = "PersonName";
        person.Age = 99;
        Console.WriteLine($"Person details - Name = {person.Name}, Age = {person.Age}");

        // Increment the Age property:
        person.Age += 1;
        Console.WriteLine($"Person details - Name = {person.Name}, Age = {person.Age}");

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}

/* Output:
Person details - Name = N/A, Age = 0
Person details - Name = PersonName, Age = 99
Person details - Name = PersonName, Age = 100
*/

访问器语法和编码技术

除了声明属性的基本语法之外,C# 还包括可用于编写更简洁和表达性代码的语法。 这些功能包括:

  • Expression-Bodied 成员
  • 字段支持的属性
  • 必需属性

Expression-Bodied 成员

表达式主体成员为编写单行属性访问器提供更简洁的语法。

注意

表达式主体成员也可以应用于方法、索引器和事件访问器。

属性访问器通常由分配或返回表达式结果的单行语句组成。 在本单元前面检查的 Person 类是一个很好的示例:


class Student
{
    private string? _name;  // the name field
    public string Name    // the Name property
    {
        get
        {
            return _name != null ? _name : "NA";
        }
        set
        {
            _name = value;
        }
    }
}

在此示例中,Name 属性使用单行语句返回 _name 字段的值(如果不是 null)或字符串“NA”(如果为 null)。 set 访问器使用单行语句将 Name 属性的值分配给 _name 字段。

表达式正文定义由 => 标记组成,后跟用于分配或检索属性值的表达式。 由于为 Student 类定义的访问器实现单行语句,因此它非常适合使用表达式正文定义进行更新。

以下代码片段使用适用于 get 和 set 访问器的表达式主体定义来更新 Student 类:


class Student
{
    private string? _name;  // the name field
    public string Name    // the Name property
    {
        get => _name ?? "NA";
        set => _name = value;
    }
}

字段支持的属性

从 C# 13 开始,可以使用 field 关键字预览功能在属性访问器中添加验证或其他逻辑。 field 关键字用于访问编译器为属性合成的支持字段。 它使你可以编写属性访问器,而无需显式声明单独的后盾字段。


public class Person
{
    public string? FirstName 
    { 
        get;
        set => field = value.Trim(); 
    }

    // Omitted for brevity.
}

重要

field 关键字是 C# 13 中的预览功能。 必须使用 .NET 9,并将 <LangVersion> 元素设置为在项目文件中预览,才能使用 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 或 default是有效的。 如果类型不可为 null,例如这些示例中的 string,编译器会发出警告。


var aPerson = new Person("PersonName");
aPerson = new Person{ FirstName = "PersonName"};
// Error CS9035: Required member `Person.FirstName` must be set:
//aPerson2 = new Person();