定义属性

  属性定义方式与字段类似,但包含的内容比较多。如前所述,属性涉及的内容比字段多,是因为它们在修改状态前还可以执行一些额外的操作,实际上,它们可能并不修改状态。属性拥有两个类似于函数的块,一个块用于获取属性的值,另一块用于设置属性的值。

  这两个块也称为访问器,分别用 getset 关键字来定义,可以用于控制对属性的访问级别。可以忽略其中的一个块来创建只读或只写属性(忽略 get 块创建只写属性,忽略 set 块创建只读属性)。当然,这仅适用于外部代码,因为类中的其他代码可以访问这些代码块能访问的数据。还可以在访问器上包含可访问修饰符,例如使 get 块变成公共的,把 set 块变成受保护的。只有包含其中一个块,才能获得有效属性(即不能读取也不能修改的属性没有任何用处)。

  属性的基本结构包括标准的可访问修饰符(publicprivate 等),后跟类名、属性名和 get 块(或 set 块,或者 get 块和 set 块,其中包含属性处理代码),例如:

  1. public int MyIntProp
  2. {
  3. get
  4. {
  5. // Property get code.
  6. }
  7. set
  8. {
  9. // Progerty set code.
  10. }
  11. }
  .NET 中的公共属性也以 PascalCasing 方式来命名,而不是 camelCasing 方式命名,与字段和方法一样,这里使用 PascalCasing 方式。

  定义代码中的第一行非常类似于定义字段的代码。区别是行末没有分号,而是一个包含嵌套 getset 块的代码块。

  get 块必须有一个属性类型的返回值,简单的属性一般与私有字段相关联,以控制对这个字段的访问,此时 get 块可以直接返回该字段的值,例如:

  1. // Field used by property
  2. private int myInt;
  3. // Property.
  4. public int MyIntProp
  5. {
  6. get
  7. {
  8. return myInt;
  9. }
  10. set
  11. {
  12. // Property set code.
  13. }
  14. }

  类外部的代码不能直接访问这个 myInt 字段,因为其访问级别是私有的。外部的代码必须使用属性来访问该字段。set 函数以类似的方式把一个值赋给字段。这里可以使用关键字 value 表示用户提供的属性值:

  1. // Field used by property.
  2. private int myInt;
  3. // Property.
  4. public int MyIntProp
  5. {
  6. get
  7. {
  8. return myInt;
  9. }
  10. set
  11. {
  12. myInt = value;
  13. }
  14. }

  value 等于类型与属性相同的一个值,所以如果属性和字段使用相同的类型,就不必担心数据类型转换了。

  这个简单的属性只能直接访问 myInt 字段。在对操作进行更多的控制时,属性的真正作用才能发挥出来。例如,使用下面的代码实现 set 块:

  1. set
  2. {
  3. if (value >= 0 && value <= 10)
  4. myInt = value;
  5. }

  只有赋给属性的值在0~10之间,才会改 myInt。此时,要做一个重要的设计选择:如果使用了无效值,该怎么办?有 4 种选择:

  1. 什么也不做(如上述代码所示)。
  2. 给字段赋默认值。
  3. 继续执行,就好像没有发生错误一样,但记录下该事件,以备将来分析。
  4. 抛出异常。

  一般情况下,后两个选择效果较好,选择哪个选项取决于如何使用类,以及给类的用户授予多少控制权。抛出异常给用户提供的控制权相当大,可以让它们知道发生了什么情况,并作出适当的响应。为此可以使用 System 名称空间中的标准异常,例如:

  1. set
  2. {
  3. if (value >= 0 && value <= 10)
  4. myInt = value;
  5. else
  6. throw (new ArgumentOutOfRangeException("MyIntProp", value, "MyIntProp must be assigned a value between 0 and 10."));
  7. }

  这可以在使用属性的代码中通过 try…catch…finally 逻辑来处理,详见第 7 章。

  记录数据,例如,记录到文本文件中,对产品代码会比较有效,因为产品代码不应发生错误。它们允许开发人员检查性能,如有必要,还可以调试现在的代码。

  属性可以使用 virtualoverrideabstract 关键字,就像方法一样,但这几个关键字不能用于字段。最后,如上所述,访问器可以有自己的可访问性,例如:

  1. // Field used by property.
  2. private int myInt;
  3. // Property.
  4. public int MyIntProp
  5. {
  6. get
  7. {
  8. return myInt;
  9. }
  10. protected set
  11. {
  12. myInt = value;
  13. }
  14. }

  只有类或派生类中的代码才能使用 set 访问器。

  访问器可以使用的访问修饰符取决于属性的可访问性,访问器的可访问性不能高于它所属的属性,也就是说,私有属性对它的访问器不能包含任何可访问修饰符,而公共属性可以对其访问器使用所有的可访问修饰符。下面的示例将定义和使用字段、方法和属性。

  试一试:使用字段、方法和属性:Ch10Ex01

  (1)在 C:\BegVCSharp\Chapter10 目录中创建一个新控制台应用程序项目 Ch10Ex01

  (2)使用 AddClass 快捷方式添加一个新类 MyClass,这将在新文件 MyClass.cs 中定义这个新类。

  (3)修改 MyClass.cs 中的代码,如下所示:

  1. public class MyClass
  2. {
  3. public readonly string Name;
  4. private int intVal;
  5. public int Val
  6. {
  7. get
  8. {
  9. return intVal;
  10. }
  11. set
  12. {
  13. if (value >= 0 && value <= 10)
  14. intVal = value;
  15. else
  16. throw (new ArgumentOutOfRangeException("Val", value,
  17. "Val must be assigned a value between 0 and 10."));
  18. }
  19. }
  20. public override string ToString()
  21. {
  22. return "Name: " + Name + "\nVal: " + Val;
  23. }
  24. private MyClass() : this("Default Name")
  25. {
  26. }
  27. public MyClass(string newName)
  28. {
  29. Name = newName;
  30. intVal = 0;
  31. }
  32. }

  (4)修改 Program.cs 中的代码,如下所示:

  1. static void Main(string[] args)
  2. {
  3. Console.WriteLine("Creating object myObj...");
  4. MyClass myObj = new MyClass("My Object");
  5. Console.WriteLine("myObj created.");
  6. for (int i = -1; i <= 0; i++)
  7. {
  8. try
  9. {
  10. Console.WriteLine("\nAttempting to assign {0} to myObj.Val...", i);
  11. myObj.Val = i;
  12. Console.WriteLine("Value {0} assigned to myObj.Val.", myObj.Val);
  13. }
  14. catch (Exception e)
  15. {
  16. Console.WriteLine("Exception {0} thrown.", e.GetType().FullName);
  17. Console.WriteLine("Message:\n\"{0}\"", e.Message);
  18. }
  19. Console.WriteLine("\nOutputting myObj.ToString()...");
  20. Console.WriteLine(myObj.ToString());
  21. Console.WriteLine("myObj.ToString() Output.");
  22. Console.ReadKey();
  23. }
  24. }

  示例的说明

  Main() 中的代码创建并使用在 MyClass.cs 中定义的 MyClass 类的实例。实例化这个类必须使用非默认的构造函数来进行,因为 MyClass 类的默认构造函数是私有的:

  1. private MyClass() : this ("Default Name")
  2. {
  3. }

  注意 ⚠️,这里用 this("Default Name") 来保证,如果调用了该构造函数,Name 就获取一个值。如果这个类用于派生一个新类,这就是可能的。必须这么做,因为不给 Name 字段赋值,就会在后面产生错误。

  所使用的非默认构造函数把值赋给只读字段 Name (只能在字段声明或在构造函数中给它赋值)和私有字段 intVal

  接着,Main() 试着给 myObj (MyClass 的实例)的 Val 属性两次赋值。使用 for 循环在两次迭代中赋值 -1 和 0,使用 try…catch 结构检查抛出的任何异常。把 -1 赋给属性时,会抛出 System.ArgumentOutOfRangeException 类型的异常,catch 块中的代码会把该异常的信息输出到控制台窗口中。在下一个循环中,值 0 成功地赋给了 Val 属性,通过这个属性再把值赋给私有字段 intVal

  最后,使用重写 ToString() 方法输出一个格式化的字符串,来表示对象的内容:

  1. public override string ToString()
  2. {
  3. return "Name: " + Name + "\nVal: " + Val;
  4. }

  必须使用 override 关键字来声明这个方法,因为它重写了基类 System.Object 的虚方法 ToString()。此处的代码直接使用属性 Val,而不是私有字段 intVal。没有理由不以这种方式使用类中的属性,但这可能会对性能产生轻微影响(对性能的影响非常小,我们不会察觉到)。当然,使用属性也可以在属性中进行固有的有效性验证,对这类中的代码也是有好处的。