使用属性(C# 编程指南)

属性结合了字段和方法的多个方面。 对于对象的用户来说,属性似乎是一个字段,访问属性需要相同的语法。 对于类的实现者来说,属性可以是一个或两个代码块,用于表示 get 访问器和/或 set 或 init 访问器。 读取属性时,执行 get 访问器的代码块;向属性赋予值时,执行 set 或 init 访问器的代码块。 将不带 set 访问器的属性视为只读。 将不带 get 访问器的属性视为只写。 将具有以上两个访问器的属性视为读写。 可通过 init 访问器而不是 set 访问器使该属性能够设置为对象初始化的一部分并在其他情况下使其只读。

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

属性有许多用途:

  • 它们可在允许更改数据之前验证数据。
  • 它们可以透明地在某个类上公开数据,而该数据是从其他来源(例如数据库)检索来的。
  • 它们可在数据发生更改(例如引发事件或更改其他字段的值)时执行操作。

在类块中声明属性时,需要依次指定字段的访问级别、属性的类型、属性的名称,然后是声明get 访问器和/或 set 访问器的代码块。 例如:

public class Date
{
    private int _month = 7;  // Backing store

    public int Month
    {
        get => _month;
        set
        {
            if ((value > 0) && (value < 13))
            {
                _month = value;
            }
        }
    }
}

此示例中将 Month 声明为属性,以便 set 访问器可确保 Month 值设置在 1 至 12 之间。 Month 属性使用私有字段跟踪实际值。 属性数据的真实位置通常称为属性的“后备存储”,属性通常使用专用字段作为后备存储。 将字段被标记为私有,确保只能通过调用属性来对其进行更改。 有关公共和专用访问限制的详细信息,请参阅访问修饰符。 自动实现的属性为简单属性声明提供简化的语法。 有关详细信息,请参阅 自动实现的属性。

从 C# 14 开始,可以使用 字段支持的属性 向 set 自动实现的属性的访问器添加验证,如以下示例所示:

public class DateExample
{
    public int Month
    {
        get;
        set
        {
            if ((value > 0) && (value < 13))
            {
                field = value;
            }
        }
    }
}

get 访问器

get 访问器的结构类似于方法。 它必须返回属性类型的值。 C# 编译器和实时 (JIT) 编译器检测 get 访问器的常见实现模式并优化这些模式。 例如,一种可能将返回一个字段而不执行任何计算的get访问器优化为对该字段的内存读取。 自动实现的属性遵循此模式,并受益于这些优化。 但是,无法内联虚拟 get 访问器方法,因为编译器在编译时不知道在运行时实际可调用哪些方法。 以下示例显示一个 get 访问器,它返回私有字段 _name 的值:

class Employee
{
    private string _name;           // the name field
    public string Name => _name;    // the Name property
}

引用属性时,除了作为赋值目标外,还调用 get 访问器读取属性值。 例如:

var employee = new Employee();
//...

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

get 访问器必须是表达式主体成员,或者在 return 或 throw 语句中结束,且控制不能超出访问器主体。

警告

使用访问器更改对象的 get 状态通常是一种错误的编程样式。 此规则的一个例外是 延迟计算 的属性,其中仅当首次访问属性时,才会计算属性的值。

get 访问器可以用于返回字段值或计算并返回字段值。 例如:

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

在前面的示例中,如果不向 Name 属性赋值,则返回值 NA。

set 访问器

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

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

向属性赋值时,通过使用提供新值的自变量调用 set 访问器。 例如:

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

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

在 value 访问器中,使用隐式参数名 set 进行本地变量声明是错误的。

初始化访问器

用于创建 init 访问器的代码与用于创建 set 访问器的代码相同,只不过前者使用的关键字是 init 而不是 set。 不同之处在于,init 访问器只能在构造函数中使用,或通过对象初始值设定项使用。

备注

可以将属性标记为 public、private、protected、internal、protected internal 或 private protected。 这些访问修饰符定义该类的用户访问该属性的方式。 相同属性的 get 和 set 访问器可以具有不同的访问修饰符。 例如,get 可能为 public允许从类型外部进行只读访问;而 set 可能为 private 或 protected。 有关详细信息,请参阅访问修饰符。

可以通过使用 static 关键字将属性声明为静态属性。 静态属性可供调用方在任何时候使用,即使不存在类的任何实例。 有关详细信息,请参阅静态类和静态类成员。

可以通过使用 virtual 关键字将属性标记为虚拟属性。 派生类可以通过虚拟属性使用 override 关键字覆盖属性行为。 有关这些选项的详细信息,请参阅继承。

重写虚拟属性的属性也可以被sealed关键字修饰,这样一来,对于派生类,该属性将不再是虚拟的。 最后,可以将属性声明为 abstract。 抽象属性不定义类中的实现,派生类必须写入自己的实现。 有关这些选项的详细信息,请参阅抽象类、密封类及类成员。

注意

使用虚拟、抽象或重写修饰符在静态属性的访问器上是错误的。

示例

此示例演示实例、静态和只读属性。 它接收通过键盘键入的员工姓名,按 1 递增 NumberOfEmployees,并显示员工姓名和编号。

public class Employee
{
    public static int NumberOfEmployees;
    private static int _counter;
    private string _name;

    // A read-write instance property:
    public string Name
    {
        get => _name;
        set => _name = value;
    }

    // A read-only static property:
    public static int Counter => _counter;

    // A Constructor:
    public Employee() => _counter = ++NumberOfEmployees; // Calculate the employee's number:
}

隐藏属性示例

此示例演示如何访问基类中的属性,即使该属性在派生类中被另一个同名属性隐藏:

public class Employee
{
    private string _name;
    public string Name
    {
        get => _name;
        set => _name = value;
    }
}

public class Manager : Employee
{
    private string _name;

    // Notice the use of the new modifier:
    public new string Name
    {
        get => _name;
        set => _name = value + ", Manager";
    }
}

class TestHiding
{
    public static void Test()
    {
        Manager m1 = new Manager();

        // Derived class property.
        m1.Name = "John";

        // Base class property.
        ((Employee)m1).Name = "Mary";

        System.Console.WriteLine($"Name in the derived class is: {m1.Name}");
        System.Console.WriteLine($"Name in the base class is: {((Employee)m1).Name}");
    }
}
/* Output:
    Name in the derived class is: John, Manager
    Name in the base class is: Mary
*/

以下内容是前一示例中的要点:

  • 派生类中的属性 Name 隐藏基类中的属性 Name。 在这种情况下,new 修饰符用于派生类的属性声明中:
    public new string Name
    
  • (Employee) 转换用于访问基类中的隐藏属性:
    ((Employee)m1).Name = "Mary";
    

有关隐藏成员的详细信息,请参阅 new 修饰符。

Override 属性示例

在此示例中,两个类(Cube 和 Square)实现抽象类 Shape,并重写其抽象 Area 属性。 请注意属性上的 override 修饰符的使用。 程序接受边长作为输入,计算正方形的面积和立方体的体积。 它还接受将面积作为输入,计算正方形和立方体的相应边长。

abstract class Shape
{
    public abstract double Area
    {
        get;
        set;
    }
}

class Square : Shape
{
    public double side;

    //constructor
    public Square(double s) => side = s;

    public override double Area
    {
        get => side * side;
        set => side = System.Math.Sqrt(value);
    }
}

class Cube : Shape
{
    public double side;

    //constructor
    public Cube(double s) => side = s;

    public override double Area
    {
        get => 6 * side * side;
        set => side = System.Math.Sqrt(value / 6);
    }
}

class TestShapes
{
    static void Main()
    {
        // Input the side:
        System.Console.Write("Enter the side: ");
        double side = double.Parse(System.Console.ReadLine());

        // Compute the areas:
        Square s = new Square(side);
        Cube c = new Cube(side);

        // Display the results:
        System.Console.WriteLine($"Area of the square = {s.Area:F2}");
        System.Console.WriteLine($"Area of the cube = {c.Area:F2}");
        System.Console.WriteLine();

        // Input the area:
        System.Console.Write("Enter the area: ");
        double area = double.Parse(System.Console.ReadLine());

        // Compute the sides:
        s.Area = area;
        c.Area = area;

        // Display the results:
        System.Console.WriteLine($"Side of the square = {s.side:F2}");
        System.Console.WriteLine($"Side of the cube = {c.side:F2}");
    }
}
/* Example Output:
    Enter the side: 4
    Area of the square = 16.00
    Area of the cube = 96.00

    Enter the area: 24
    Side of the square = 4.90
    Side of the cube = 2.00
*/

请参阅