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

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

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

属性有许多用途:

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

通过依次指定字段的访问级别、属性类型、属性名、声明 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# 13 开始,可以使用字段支持的属性向set自动实现的属性的访问器添加验证,如以下示例所示:

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

重要

关键字 field 是 C# 13 中的预览功能。 必须使用 .NET 9 并将元素preview设置为<LangVersion>项目文件中,才能使用field上下文关键字。

应注意在 field 具有名为 field字段的类中使用关键字功能。 新 field 关键字将隐藏属性访问器范围中命名 field 的字段。 可以更改变量的名称 field ,或使用 @ 令牌将标识符引用 field@field。 可以通过阅读关键字的功能规范field来了解详细信息。

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 访问器必须是表达式主体成员,或在 returnthrow 语句中结束,且控件无法流出访问器主体。

警告

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

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

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

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

set 访问器

set 访问器类似于返回类型为 void 的方法。 它使用名为 value 的隐式参数,该参数的类型为属性的类型。 编译器和 JIT 编译器还识别 setinit 访问器的常见模式。 这些常见模式经过优化,直接写入支持字段的内存。 在下面的示例中,将 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

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

init 访问器

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

备注

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

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

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

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

注意

static 属性的访问器上使用 virtualabstractoverride 修饰符是错误的。

示例

此示例演示实例、静态和只读属性。 它接收通过键盘键入的员工姓名,按 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:
}

Hidden 属性示例

此示例演示如何访问由派生类中同名的另一属性隐藏的基类中的属性:

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: {0}", m1.Name);
        System.Console.WriteLine("Name in the base class is: {0}", ((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 属性示例

在此示例中,两个类(CubeSquare)实现抽象类 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 = {0:F2}", s.Area);
        System.Console.WriteLine("Area of the cube = {0:F2}", c.Area);
        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 = {0:F2}", s.side);
        System.Console.WriteLine("Side of the cube = {0:F2}", c.side);
    }
}
/* 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
*/

请参阅