UML软件工程组织

特别的 .NET 类型成员
转自:www.microsoft.com 作者:Jeffrey Richter

本页内容

类型构造函数 类型构造函数
属性 属性
索引属性 索引属性
小结 小结

类型构造函数

您应该很熟悉构造函数,它负责设置对象实例的初始状态。除了实例构造函数,Microsoft® .NET 公共语言运行库(common language runtime,CLR)还支持类型构造函数(也称为静态构造函数、类构造函数、或者类型初始函数)。类型构造函数可以用于接口、类和值类型。它允许类型在任何成员被访问前进行所需的初始化工作。类型构造函数不接受任何参数,返回类型必须是 void。类型构造函数仅访问类型的静态字段,并且其通常的用途是初始化这些字段。类型构造函数确保在类型的任何实例创建前,以及在类型的任何静态字段或者方法被引用前运行。

许多语言(包括 C#)自动为任何定义的类型产生类型构造函数。但是,有些语言需要显式实现类型构造函数。

为了理解类型构造函数,查看下面的类型(用 C# 定义):

class AType {
   static int x = 5;
}

在生成该段代码时,编译器自动为 AType 产生一个类型构造函数。该构造函数负责将静态字段 x 的值初始化为 5。如果使用 ILDasm,可以很容易地发现类型构造函数方法,因为它们的名字是 .cctor(即 class constructor)。

在 C# 中,您可以在类型中定义一个静态的构造函数方法来亲自实现类型构造函数。使用 static 关键字让该构造函数成为一个类型构造函数而不是实例构造函数。下面是一个很简单的例子:

class AType {
   static int x;
   static AType() {
      x = 5;
   }
}

该类型定义与前面的相同。注意,类型构造函数永远不能试图创建其自身类型的实例,并且构造函数不能引用任何此类型的非静态成员。

最后,对于以下代码,C# 编译器只产生一个类型构造函数方法。

class AType {
   static int x = 5;
   static AType() {
      x = 10;
   }
}

该构造函数首先将 x 初始化为 5,然后将 x 初始化为 10。换句话说,编译器最终产生的类型构造函数首先包含静态字段的初始化代码,然后才是类型构造函数方法中的代码。

属性

许多类型定义可进行检索或者改变的属性。通常来说,这些属性被实现为类型的字段成员。例如,下面是一个包含两个字段的类型定义:

class Employee {
   public String Name;
   public Int32 Age;
}

如果创建该类型的一个实例,就能够轻松获取或设置以下属性,如下所示:

Employee e = new Employee();
e.Name = "Jeffrey Richter"; // Set the Name attribute
e.Age = 36;                 // Set the Age attribute
Console.WriteLine(e.Name);  // Displays "Jeffrey Richter" 

以这种方式对属性进行操作很常见。但是,我认为,上面的代码根本无法实现。面向对象设计和开发的一大特点是数据抽象。数据抽象意味着类型字段永远不应该被公开,因为太容易编写出不恰当使用字段的代码,破坏对象的状态。例如,很容易写出以下代码来破坏一个Employee对象。

e.Age = -5; // How could someone be -5 years old?

因此,当设计类型时,强烈建议将所有的字段设为私有,或者至少受保护 - 永远不要设为公共。这样,要让某类型的用户获取或者设置属性,公开专门用于此目的的方法。将对字段的访问封装起来的方法通常称为访问器方法 (accessor)。它们可以进行完整的检查(可选),保证对象的状态不会被破坏。例如,我会重写前面所示的 Employee 类,以模仿HYPERLINK "http://msdn.microsoft.com/msdnmag/issues/01/02/dotnet/figures.asp" \l "fig1"图 1 中的代码。这是一个简单的例子,但是可以看到抽象数据字段的巨大好处,也可以看到使属性只读或只写是多么容易,无需实现某种访问器方法。

Figure 1 Abstracting Data Fields

class Employee {
private String Name; // field is now private
private Int32 Age; // field is now private

public String GetName() {
return(Name);
}

public void SetName(String value) {
Name = value;
}

public Int32 GetAge() {
return(Age);
}

public void SetAge(Int32 value) {
if (value <= 0)
throw(new ArgumentException("Age must be greater than 0");
Age = value;
}
}

图1

图 1 中所示的抽象数据的方式有两个缺点。首先,因为必须实现额外的函数,所以需要编写更多的代码;其次,使用此类型的用户必须调用方法,而不是简单地引用一个字段名。

e.SetAge(36);    // Updates the age
e.SetAge(-5);    // Throws an exception 

我想您也会觉得这些缺点问题并不大。不过,运行库提供一种称为属性 (property) 的机制,可以部分解决第一个缺点并完全克服第二个缺点。


Figure 2 Using Get and Set Properties

class Employee {
private String _Name; // prepended '_' to avoid conflict
private Int32 _Age; // prepended '_' to avoid conflict

public String Name {
get { return(_Name); }
set { _Name = value; } // 'value' always identifies the new value
}

public Int32 Age {
get { return(_Age); }
set {
if (value <= 0) // 'value' always identifies the new value
throw(new ArgumentException("Age must be greater than 0");
_Age = value;
}
}
}

图2

图 2 中所示的类使用了与图 1 中的类功能完全相同的属性。您可以看到,属性稍微简化了代码,更重要的是,它们允许调用方按如下方式编写代码:

e.Age = 36;    // Updates the age
e.Age = -5;    // Throws an exception

Get 属性访问器 (get property accessor) 返回的值与传递给 Set 属性访问器 (set property accessor) 的参数是同一类型。Set 属性具有返回类型 void,Get 属性没有任何参数。属性可以是静态函数,虚函数,抽象函数,内部函数,私有函数,保护函数或者公有函数。另外,后面会提到,属性可以在接口中定义。

还应该指出,属性不一定和某个字段相联系。例如,System.IO.FileStream 类型定义了一个 Length 属性,它返回流的字节总数。在字段中不保留该长度;相反,在调用 Length 属性的 get 方法时,它调用另一个函数,使底层操作系统返回打开文件流的字节总数。

当创建属性时,编译器实际上产生特别的 get_PropName 和/或 set_PropName 访问器方法(PropName 是属性的名称)。大多数编译器会理解这些特别的方法,从而允许开发人员使用特别的属性语法来访问这些方法。但是,不需要遵从通用语言规范(Common Language Specification,CLS)的编译器完全支持属性,编译器只需要支持调用这些特别的访问器方法。

另外,不完全支持属性的编译器可能需要稍微不同的语法来定义和使用属性。例如,托管扩展的 C++ 需要使用 _property 关键字。

索引属性

有些类型(例如 System.Collections.SortedList)公开元素的逻辑列表。为了轻松访问这种类型的元素,类型可以定义索引属性(也称为索引器,indexer)。图 3 显示索引属性的一个示例。使用这种类型索引器非常简单:

Figure 3 Using Index Properties

class BitArray {
private Byte[] byteArray;
public BitArray(int numBits) {
if (numBits <= 0)
throw new ArgumentException("numBits must be > 0");
byteArray = new Byte[(numBits + 7) / 8];
}

public Boolean this[Int32 bitPosition] {
get {
if ((bitPosition < 0) ||
(bitPosition > byteArray.Length * 8 - 1))
throw new IndexOutOfRangeException();
return((byteArray[bitPosition / 8] &
(1 << (bitPosition % 8))) != 0);
}
set {
if ((bitPosition < 0) ||
(bitPosition > byteArray.Length * 8 - 1))
throw new IndexOutOfRangeException();
if (value) {
byteArray[bitPosition / 8] = (Byte)
(byteArray[bitPosition / 8] |
(1 << (bitPosition % 8)));
} else {
byteArray[bitPosition / 8] = (Byte)
(byteArray[bitPosition / 8] &
~(1 << (bitPosition % 8)));
}
}
}
}

图3
BitArray ba = new BitArray(14);
for (int x = 0; x < 14; x++) {
   // Turn all even numbered bits on
   ba[x] = (x % 2 == 0);
   Console.WriteLine("Bit " + x + " is " + (ba[x] ? "On" : "Off"));
}

在图 3 所示的 BitArray 示例中,索引器有一个 Int32 的参数:bitPosition。所有索引器必须至少有一个参数,而且可能有两个或者更多的参数。这些参数(和返回类型)可以是任意类型。常见的情况是,创建一个将 String 作为参数的索引器,用于在一个关联数组中查找值。类型可以提供多个重载的索引器,只要它们的原型不同。

象设置属性一样,设置索引器访问器方法包含一个隐含的参数 value,它表示访问器被调用时新的期望值。BitArray 设置访问器显示这个使用的 value 参数。

一个设计良好的索引器应该同时具有 get 和 set 访问器。尽管您可以只实现 get 访问器(用于只读语义)或只实现 set 访问器(用于只写语义),建议索引器还是实现两个访问器。原因很简单,使用索引的用户不会希望只有一半的行为可用。例如,用户不希望在编写下面两行代码时出现编译器错误:

String s = SomeObj[5];  // Compiles OK if get accessor exists
SomeObj[5] = s;         // Compiler error if set accessor doesn't exist

索引器总是作用在特定的类型实例上,并且不能声明为静态的。但是索引器可以被标记为公有、私有、保护或者内部。

当创建一个索引器属性时,编译器实际上产生特殊的 get_item 和/或 set_item 访问器方法。大多数编译器会理解这些特殊的方法,从而允许开发人员使用特殊的索引属性语法访问这些方法。但是,不需要遵循 CLS 的编译器完全支持索引器属性;编译器只需要支持调用特殊的访问器方法。

而且,完全支持索引属性的编译器可能需要稍微不同的语法来定义和使用这些属性。例如,托管扩展的 C++需要使用 _property 关键字。

小结

本专栏讨论的概念对所有 .NET 程序员而言都非常重要。我提到的特殊类型的成员使组件成为公用语言运行库中最好的元素,即现代的组件旨在支持属性。

 

 

版权所有:UML软件工程组织