C--2012-说明指南-二-

40 阅读1小时+

C# 2012 说明指南(二)

原文:Illustrated C# 2012

协议:CC BY-NC-SA 4.0

六、关于类的更多信息

类成员

前两章讨论了九种类成员中的两种:字段和方法。在这一章中,我将介绍除事件和操作符之外的所有其他类成员,并解释它们的特性。我将在第十四章中讲述事件。

表 6-1 显示了类成员类型的列表。已经推出的产品标有钻石。本章涉及的内容都标有勾号。那些将在后面的文本中涉及的内容用空的复选框标记。

Image

成员修饰符的顺序

之前,您看到了字段和方法的声明可以包含修饰符,如publicprivate。在这一章中,我将讨论一些额外的修饰语。因为这些修饰语中的许多可以一起使用,所以出现的问题是,它们需要什么样的顺序?

类成员声明语句由以下部分组成:核心声明、一组可选的修饰符,以及一组可选的属性。用于描述该结构的语法如下。方括号表示包含的组件集是可选的。

   [ attributes ] [ modifiers ]  CoreDeclaration

可选组件如下:

  • modifier
    • If there is a modifier, it must be placed before the core declaration.
    • If there are multiple modifiers, they can be arranged arbitrarily.
  • attribute
    • If there is an attribute, it must be placed before the modifier and the core declaration.
    • If there are multiple attributes, you can sort them arbitrarily.

到目前为止,我只解释了两个修饰符:publicprivate。我会在第二十四章中讲述属性。例如,publicstatic都是修饰符,可以一起用来修改某些声明。因为它们都是修饰语,所以可以按任意顺序排列。下面两行在语义上是等价的:

`   public static int MaxVal;

   static public int MaxVal;`

图 6-1 显示了应用到目前为止显示的成员类型的组件顺序:字段和方法。注意,字段的类型和方法的返回类型不是修饰符——它们是核心声明的一部分。

Image

***图 6-1。*属性、修饰符和核心声明的顺序

实例类成员

类成员可以与类的一个实例相关联,也可以与整个类相关联;也就是说,应用于该类的所有实例。默认情况下,成员与实例相关联。你可以认为一个类的每个实例都有自己的每个类成员的副本。这些成员被称为实例成员

对一个实例字段值的更改不会影响任何其他实例中成员的值。到目前为止,您看到的字段和方法都是实例字段和实例方法。

例如,下面的代码声明了一个类D,带有一个整数字段Mem1Main创建该类的两个实例。每个实例都有自己的字段Mem1副本。更改字段的一个实例副本的值不会影响另一个实例副本的值。图 6-2 显示了D类的两个实例。

`   class D    {       public int Mem1;    }

   class Program    {       static void Main()       {          D d1 = new D();          D d2 = new D();          d1.Mem1 = 10; d2.Mem1 = 28;

         Console.WriteLine("d1 = {0}, d2 = {1}", d1.Mem1, d2.Mem1);       }    }`

该代码产生以下输出:


d1 = 10, d2 = 28


Image

***图 6-2。*D 类的每个实例都有自己的 Mem1 字段副本。

静态字段

除了实例字段,类还可以有所谓的静态字段。

  • The static field is shared by all instances of class , and all instances access the same memory location. Therefore, if one instance changes the value of the memory location, all instances can see the change.
  • Use the static modifier to declare a static field as follows:

   class D    {       int Mem1;                     // Instance field       <ins>static</ins> int Mem2;              // Static field         ↑    }  Keyword

例如,图 6-3 中左边的代码用静态字段Mem2和实例字段Mem1声明了类DMain定义了类D的两个实例。该图显示静态字段Mem2与任何实例的存储器分开存储。实例内部的灰色字段表示这样一个事实,即从实例方法内部,访问或更新静态字段的语法与访问任何其他成员字段的语法相同。

  • Because Mem2 is static, two instances of class D share a Mem2 field. If Mem2 is changed, the change can be seen from both.
  • Member Mem1 does not declare static, so each instance has its own different copy.

Image

***图 6-3。*静态和实例数据成员

从类外访问静态成员

在前一章中,您看到了点语法符号用于从类外部访问public实例成员。点语法表示法包括列出实例名,后跟一个点,再后跟成员名。

像实例成员一样,静态成员也可以使用点语法符号从类外部访问。但是由于没有实例,所以必须使用类名,如下所示:

   Class name     ↓     D.Mem2 = 5;            // Accessing the static class member        ↑      Member name

静态字段的例子

以下代码通过添加两个方法扩展了前面的类D:

  • One method sets the values of two data members.
  • Another method displays the values of two data members.

`   class D {       int        Mem1;       static int Mem2;

      public void SetVars(int v1, int v2) // Set the values       {  Mem1 = v1; Mem2 = v2; }                       ↑ Access as if it were an instance field

      public void Display( string str )       {  Console.WriteLine("{0}: Mem1= {1}, Mem2= {2}", str, Mem1, Mem2); }    }                                                   Access as if it were an instance field    class Program {       static void Main()       {          D d1 = new D(), d2 = new D();   // Create two instances.

         d1.SetVars(2, 4);               // Set d1's values.          d1.Display("d1");

         d2.SetVars(15, 17);             // Set d2's values.          d2.Display("d2");

         d1.Display("d1");       // Display d1 again and notice that the       }                          // value of static member Mem2 has changed!    }`

这段代码产生以下输出:


d1: Mem1= 2, Mem2= 4 d2: Mem1= 15, Mem2= 17 d1: Mem1= 2, Mem2= 17


静态成员的生存期

静态成员的生存期不同于实例成员的生存期。

  • As you saw before, instance members appear when the instance is created and disappear when the instance is destroyed. however
  • Static members exist and are accessible , even if there is no instance of the class .

图 6-4 展示了一个类D,带有一个静态字段Mem2。尽管Main没有定义该类的任何实例,但它将值5赋给静态字段,并毫无问题地打印出来。

Image

***图 6-4。*没有类实例的静态字段仍然可以被赋值和读取,因为字段与类相关联,而不是与实例相关联。

图 6-4 中的代码产生以下输出:


Mem2 = 5


Image 注意即使没有类的实例,静态成员仍然存在。如果一个静态字段有一个初始化器,那么这个字段在使用这个类的任何静态字段之前被初始化,但是不一定在程序执行的开始。

静态函数成员

除了静态字段,还有静态函数成员。

  • Static function members, like static fields, are independent of any class instance. Even if there is no instance of the class, you can still call static methods.
  • Static function members cannot access instance members. However, they can access other static members.

例如,以下类包含一个静态字段和一个静态方法。注意,静态方法的主体访问静态字段。

    class X     {        static public int A;                               // Static field        static public void PrintValA()                     // Static method        {           Console.WriteLine("Value of A: {0}", A);     }                                          ↑   }                                        Accessing the static field

以下代码使用前面代码中定义的类X:

   class Program    {       static void Main()       {          X.A = 10;               // Use dot-syntax notation          X.PrintValA();          // Use dot-syntax notation       }  ↑    }  Class name

该代码产生以下输出:


Value of A: 10


图 6-5 说明了前面的代码。

Image

***图 6-5。*一个类的静态方法可以被调用,即使这个类没有实例。

其他静态类成员类型

在表 6-2 中显示了可以声明static的类成员类型。其他成员类型不能声明为static

Image

成员常数

成员常量类似于上一章介绍的局部常量,只是它们是在类声明中声明的,而不是在方法中声明的,如下例所示:

`   class MyClass    {        const int IntVal = 100;              // Defines a constant of type int               ↑           ↑                // with a value of 100.    }         Type        Initializer

   const double PI = 3.1416;               // Error: cannot be declared outside a type                                            // declaration`

与局部常量一样,用于初始化成员常量的值必须在编译时可计算,并且通常是预定义的简单类型之一或由它们组成的表达式。

   class MyClass    {       const int IntVal1 = 100;       const int IntVal2 = 2 * IntVal1;  // Fine, since the value of IntVal1    }                                    // was set in the previous line.

与局部常数一样,不能在声明成员常数后将其赋值。

   class MyClass    {       const int IntVal;                // Error: initialization is required.       IntVal = 100;                    // Error: assignment is not allowed.    }

Image 注意与 C 和 C++不同,C# 中没有全局常量。每个常数都必须在类型中声明。

常数如静力学

然而,成员常量比局部常量更有趣,因为它们的行为类似于静态值。它们对该类的每个实例都是“可见”的,即使没有该类的实例,它们也是可用的。与实际的静态不同,常量没有自己的存储位置,在编译时由编译器以类似于 C 和 C++中的#define值的方式替换。

例如,下面的代码用常量字段PI声明了类XMain没有创建X的任何实例,但是它可以使用字段PI并打印它的值。图 6-6 说明了代码。

`   class X    {       public const double PI = 3.1416;    }

   class Program    {       static void Main()       {          Console.WriteLine("pi = {0}", X.PI);    // Use static field PI       }    }`

该代码产生以下输出:


pi = 3.1416


Image

***图 6-6。*常量字段的行为类似静态字段,但在内存中没有存储位置。

尽管常量成员的行为类似于静态,但是您不能将常量声明为static,如下面的代码行所示。

   static const double PI = 3.14;     // Error: can't declare a constant as static

属性

属性是表示类或类实例中数据项的成员。使用属性看起来非常像对字段进行写入或读取。语法是一样的。

例如,下面的代码展示了一个名为MyClass的类的用法,它既有一个公共字段又有一个公共属性。从它们的用法来看,你无法区分它们。

`   MyClass mc = new MyClass();

   mc.MyField    = 5;                               // Assigning to a field    mc.MyProperty = 10;                              // Assigning to a property

   WriteLine("{0} {1}", mc.MyField, mc.MyProperty); // Read field and property`

像字段一样,属性具有以下特征:

  • Is a named class member.
  • There is one type of it.
  • It can be assigned to and read from.

然而,与字段不同,属性是函数成员,因此:

  • It does not necessarily allocate memory for data storage.
  • It executes the code.

一个属性是两个匹配方法的命名集合,称为访问器

  • set Accessors are used to assign values to attributes.

get

图 6-7 显示了一个属性的表示。左边的代码显示了声明类型为int的名为MyValue的属性的语法。右边的图像显示了属性在文本中是如何可视化表示的。请注意,访问器显示在后面,因为,您很快就会看到,它们不是可直接调用的。

Image

***图 6-7。*一个名为 MyValue 的示例属性,类型为 int

属性声明和访问器

setget访问器有预定义的语法和语义。您可以将set访问器视为一个具有单个参数的方法,该参数“设置”属性值。get访问器没有参数,返回属性值。

  • set The accessor always has the following contents:
    • A single implicit value parameter named value, and the property
    • The return type of is the same void
  • get The accessor always has the following contents:
    • No parameter
    • An and attribute

的返回类型相同

图 6-8 显示了属性声明的结构。注意在图中,两个访问器声明都没有显式参数或返回类型声明。他们不需要它们,因为它们是中隐含的财产类型。

Image

***图 6-8。*属性声明的语法和结构

set访问器中的隐式参数value是一个正常值参数。像其他值参数一样,您可以使用它将数据发送到方法体中,或者在本例中,发送到访问器块中。一旦进入程序块,就可以像普通变量一样使用value,包括给它赋值。

关于访问器的其他要点如下:

  • All paths implemented through get accessor must contain a return statement that returns the attribute type value.
  • set and get accessors can be declared in any order, and there are no other methods except these two accessors on an attribute.
一个属性的例子

下面的代码展示了一个名为C1的类的声明示例,它包含一个名为MyValue的属性。

  • Note that the attribute itself does not have any storage. Instead, the accessor decides how to handle the incoming data and the data that should be sent out. In this case, the attribute is stored in a field named TheRealValue.
  • set The accessor gets its input parameter value and assigns the value to the field TheRealValue.
  • get The accessor only returns the value of field TheRealValue.

图 6-9 说明了代码。

`   class C1    {       private int TheRealValue;               // Field: memory allocated

      public int MyValue                      // Property: no memory allocated       {          set          {             TheRealValue = value;          }

         get          {             return TheRealValue;          }       }    }` Image

***图 6-9。*属性访问器经常使用字段进行存储

使用属性

如前所述,您可以像访问字段一样读写属性。访问器是隐式调用的。

  • To write an attribute, use the attribute name to the left of the assignment statement.
  • To read an attribute, use the name of the attribute in the expression.

例如,下面的代码包含一个名为MyValue的属性声明的概要。您只使用属性名写入和读取属性,就像它是一个字段名一样。

   int MyValue             // Property declaration    {       set{ ... }       get{ ... }    }    ...    Property name       ↓    MyValue = 5;            // Assignment: the set method is implicitly called.    z = MyValue;            // Expression: the get method is implicitly called.           ↑    Property name

根据您是写入属性还是读取属性,隐式调用适当的访问器。您不能显式调用访问器。尝试这样做会产生编译错误。

   y = MyValue.get();      // Error! Can't explicitly call get accessor.    MyValue.set(5);         // Error! Can't explicitly call set accessor.

属性和关联字段

一个属性通常与一个字段相关联,如前两节所示。一种常见的做法是通过声明字段private和声明属性public将字段封装在类中,以提供从类外部对字段的受控访问。与资产相关联的字段被称为后台字段后台存储

例如,以下代码使用公共属性MyValue对私有字段TheRealValue进行受控访问:

`   class C1    {       private int TheRealValue = 10;   // Backing Field: memory allocated       public  int MyValue              // Property: no memory allocated       {          set{ TheRealValue = value; }  // Sets the value of field TheRealValue          get{ return TheRealValue; }   // Gets the value of the field       }    }

   class Program    {       static void Main()    {                                       Read from the property as if it were a field.          C1 c = new C1();                       ↓              Console.WriteLine("MyValue:  {0}", c.MyValue);

         c.MyValue = 20;        ← Use assignment to set the value of a property.          Console.WriteLine("MyValue:  {0}", c.MyValue);       }    }`

有几种命名属性及其支持字段的惯例。一个惯例是对两个名称使用相同的字符串,但是对字段使用骆驼大小写,对属性使用帕斯卡大小写。(Camel case 描述了一个复合词标识符,其中每个词的第一个字母,除了第一个,都是大写的,其余的字母都是小写的。Pascal 大小写是复合词中每个单词的第一个字母大写的地方。)虽然这违反了一般规则,即不同的标识符只有大小写不同是不好的做法,但它的优点是以一种有意义的方式将两个标识符联系在一起。

另一个约定是对属性使用 Pascal 大小写,然后对字段使用相同标识符的 camel 大小写版本,前面带下划线。

以下代码显示了这两种约定:

`   private int firstField;                  // Camel casing    public  int FirstField                   // Pascal casing    {       get { return firstField; }       set { firstField = value; }    }

   private int _secondField;                // Underscore and camel casing    public  int SecondField    {       get { return _secondField; }       set { _secondField = value; }    }`

执行其他计算

属性访问器不仅限于从关联的后台字段来回传递值;getset访问器可以执行任何计算,或者不执行任何计算。唯一需要的动作get访问器返回一个属性类型的值。

例如,下面的例子展示了一个有效(但可能没用)的属性,当调用它的get访问器时,它只返回值5。当调用set访问器时,它不做任何事情。隐式参数value的值被忽略。

   public int Useless    {       set{  /* I'm not setting anything.                */ }       get       {     /* I'm always just returning the value 5.   */          return 5;       }    }

下面的代码展示了一个更实际、更有用的属性,其中set访问器在设置关联字段之前执行过滤。set访问器将字段TheRealValue设置为输入值,除非输入值大于 100。在这种情况下,它将TheRealValue设置为100

   int TheRealValue = 10;                     // The field    int MyValue                                // The property    {       set                                     // Sets the value of the field       {          TheRealValue = value > 100           // but makes sure it's not > 100                            ? 100                            : value;       }       get                                     // Gets the value of the field       {          return TheRealValue;       }    }

Image 注意在前面的代码示例中,等号和语句结尾之间的语法可能看起来有些奇怪。该表达式使用了条件运算符,我将在第八章的中详细介绍。条件运算符是一个三元运算符,计算问号前面的表达式,如果表达式计算结果为true,则返回问号后面的表达式。否则,它返回冒号后的表达式。有些人会在这里使用一个if...then语句,但是条件操作符更合适,当我们在第八章中详细查看这两个构造时,你会看到。

只读和只写属性

通过省略属性的声明,可以不定义属性的一个或另一个(但不是两个)访问器。

  • The attribute of only one get accessor is called read-only attribute. Read-only property is a safe way to transfer data items from a class or class instance, and does not allow excessive access.
  • Only the attribute of set accessor is called , and only the attribute of is written. Write-only property is a safe way to transfer data items from outside the class to the class without excessive access.
  • At least one of the two accessors must be defined, or the compiler will generate an error message.

图 6-10 显示了只读和只写属性。

Image

图 6-10 。属性可以有一个或另一个未定义的访问器。

属性与公共字段

作为首选的编码实践,属性优先于公共字段有几个原因:

由于属性是函数成员,而不是数据成员,它们允许你处理输入和输出,这是你不能用公共字段做的。* You can have read-only or write-only attributes, but you can't have these characteristics of a field.* The semantics of compiled variables and compiled attributes are different.

第二点在您释放被其他代码访问的程序集时有所暗示。例如,有时您可能想使用公共字段而不是属性,理由是如果您需要向字段中保存的数据添加处理,您总是可以在以后将字段更改为属性。这是真的,但是如果你做了那样的改变,你也将不得不重新编译任何其他访问该字段的程序集*,因为字段和属性的编译语义是不同的。另一方面,如果你将它实现为一个属性,并且仅仅改变它的实现,你就不需要重新编译其他访问它的程序集。*

一个计算的只读属性的例子

到目前为止,在大多数示例中,属性都与一个支持字段相关联,并且getset访问器已经引用了该字段。但是,属性不必与字段相关联。在下面的例子中,get访问器计算的返回值。

在代码中,毫不奇怪,class RightTriangle表示一个直角三角形。图 6-11 显示了只读属性Hypotenuse

  • It has two common fields, which represent the lengths of the two right-angled sides of the triangle. These fields can be written and read.
  • The third edge is represented by the attribute Hypotenuse, which is read-only and its return value is based on the lengths of the other two edges. It is not stored in the field. Instead, it will calculate the correct values for the current values of A and B as needed.

`   class RightTriangle    {       public double A = 3;       public double B = 4;       public double Hypotenuse                    // Read-only property       {          get{ return Math.Sqrt((AA)+(BB)); }    // Calculate return value       }    }

   class Program    {       static void Main()       {          RightTriangle c = new RightTriangle();          Console.WriteLine("Hypotenuse:  {0}", c.Hypotenuse);       }    }`

该代码产生以下输出:


Hypotenuse:  5


Image

图 6-11 。只读属性斜边

自动实现属性

因为属性经常与后台字段相关联,C# 提供了自动实现的属性,或者自动实现的属性,这允许你只声明属性,而不声明后台字段。编译器会为您创建一个隐藏的后台字段,并自动将getset访问器连接到该字段。

关于自动实现的属性的要点如下:

  • You don't have to declare backup fields-the compiler allocates storage space for you according to the type of attribute.
  • You cannot provide the bodies of accessors-they must simply be declared as semicolons. get serves as a simple read of the memory, and set serves as a simple write.
  • You can't access background fields except through accessors. Because you can't access it in any other way, it doesn't make sense to have read-only or write-only automatically implemented properties-so they are not allowed.

下面的代码显示了自动实现的属性的示例:

`   class C1    {                  ← No declared backing field       public int MyValue                         // Allocates memory       {          set; get;       }     ↑   ↑    }  The bodies of the accessors are declared as semicolons.

   class Program    {       static void Main()       {                        Use auto-implemented properties as regular properties.          C1 c = new C1();                         ↓          Console.WriteLine("MyValue:  {0}", c.MyValue);

         c.MyValue = 20;          Console.WriteLine("MyValue:  {0}", c.MyValue);       }    }`

该代码产生以下输出:


MyValue:  0 MyValue:  20


除了方便之外,自动实现的属性允许您轻松地将属性插入到您可能想要声明公共字段的地方。

静态属性

属性也可以声明为static。像所有静态成员一样,静态属性的访问器具有以下特征:

  • They can't access instance members of a class-although they can be accessed by them.
  • Whether this class has instances or not, they exist.
  • When you access them from outside the class, you must refer to them by class name instead of instance name.

例如,下面的代码展示了一个具有名为MyValue的自动实现的静态属性的类。在Main的前三行中,属性被访问,即使没有类的实例。Main的最后一行调用一个实例方法,该方法从类内部的访问属性。

`   class Trivial    {       public static int MyValue { get;  set; }

      public void PrintValue()               Accessed from inside the class       {                                                  ↓          Console.WriteLine("Value from inside: {0}", MyValue);       }    }

   class Program    {       static void Main()                   Accessed from outside the class       {                                              ↓                 Console.WriteLine("Init Value: {0}", Trivial.MyValue);          Trivial.MyValue = 10;           ←  Accessed from outside the class          Console.WriteLine("New Value : {0}", Trivial.MyValue);

         Trivial tr = new Trivial();          tr.PrintValue();       }    }`


Init Value: 0 New Value : 10 Value from inside: 10


实例构造函数

一个实例构造器是一个特殊的方法,每当一个类的新实例被创建时就被执行。

  • Constructors are used to initialize the state of class instances.
  • If you want to create an instance of your class from outside the class, you need to declare the constructor public.

图 6-12 显示了一个构造函数的语法。构造函数看起来像类声明中的其他方法,但有以下例外:

  • The name of the constructor is the same as the class name.
  • The constructor cannot have a return value.

Image

图 6-12 。构造函数声明

例如,下面的类使用其构造函数来初始化其字段。在这种情况下,它有一个名为TimeOfInstantiation的字段,用当前日期和时间初始化。

   class MyClass    {       DateTime TimeOfInstantiation;                        // Field       ...       public MyClass()                                     // Constructor       {          TimeOfInstantiation = DateTime.Now;               // Initialize field       }       ...    }

Image 注意刚刚完成了静态属性部分,仔细看看初始化TimeOfInstantiation的那一行。这个DateTime类(实际上是一个struct,但是你可以把它当成一个类,因为我还没有覆盖struct s)来自 BCL,NowDateTime的一个静态属性。属性创建了一个DateTime类的新实例,用系统时钟的当前日期和时间初始化它,并返回一个对新DateTime实例的引用。

带参数的构造函数

构造函数在以下方面与其他方法相似:

  • A constructor can have parameters. The syntax of the parameter is exactly the same as other methods.
  • A constructor can be overloaded.

当您使用一个对象创建表达式来创建一个类的新实例时,您可以使用new操作符,后跟该类的一个构造函数。new操作符使用该构造函数来创建类的实例。

例如,在下面的代码中,Class1有三个构造函数:一个不带参数,一个带int,另一个带stringMain使用每一个创建一个实例。

`   class Class1    {       int Id;       string Name;

      public Class1()            { Id=28;    Name="Nemo"; }   // Constructor 0       public Class1(int val)     { Id=val;   Name="Nemo"; }   // Constructor 1       public Class1(String name) { Name=name;             }   // Constructor 2

      public void SoundOff()       { Console.WriteLine("Name {0},   Id {1}", Name, Id); }    }

   class Program    {       static void Main()       {          Class1 a = new Class1(),                     // Call constructor 0.                 b = new Class1(7),                    // Call constructor 1.                 c = new Class1("Bill");               // Call constructor 2.

         a.SoundOff();          b.SoundOff();          c.SoundOff();       }    }`

该代码产生以下输出:


Name Nemo,   Id 28 Name Nemo,   Id 7 Name Bill,   Id 0


默认构造函数

如果在类声明中没有显式提供实例构造函数,则编译器会提供一个隐式的默认构造函数,该构造函数具有以下特征:

  • It doesn't need parameters.
  • It has an empty body.

如果你为一个类声明了任何构造函数*,那么编译器不会为这个类定义默认的构造函数。*

例如,以下示例中的Class2声明了两个构造函数。

  • Because there is at least one explicitly defined constructor, the compiler will not create any additional constructors.
  • In Main, an attempt was made to create a new instance with a constructor without parameters. Because has no constructor with zero parameters, the compiler will generate an error message.

`   class Class2    {       public Class2(int Value)    { ... }   // Constructor 0       public Class2(String Value) { ... }   // Constructor 1    }

   class Program    {       static void Main()       {          Class2 a = new Class2();   // Error! No constructor with 0 parameters          ...       }    }`

Image 注意你可以将访问修饰符赋给实例构造函数,就像你赋给其他成员一样。您还需要声明构造函数public,这样您就可以从类外部创建实例。你也可以创建private构造函数,它不能从类外调用,但可以在类内使用,你将在下一章看到。

静态构造函数

构造函数也可以声明为static。实例构造函数初始化一个类的每个新实例,而static构造函数在类级别初始化项目。通常,静态构造函数初始化类的静态字段。

  • Items of class level are initialized before referencing any static members.
    • Before creating any instances of the class
  • Static constructors are similar to instance constructors in the following aspects:
    • The name of the static constructor must be the same as that of the class.
    • The constructor cannot return a value.
  • Static constructors differ from instance constructors in the following aspects:
    • Static constructors use the static keyword in declarations.
    • A class can only have one static constructor and no parameters.
    • Static constructors cannot have accessibility modifiers.

下面是一个静态构造函数的示例。注意,它的形式与实例构造函数的形式相同,但是增加了关键字static

   class Class1    {       static Class1 ()       {          ...                // Do all the static initializations.       }       ...

关于静态构造函数,您应该知道的其他重要事情如下:

  • A class can have both static constructors and instance constructors.
  • Like a static method, a static constructor cannot access the instance members of its class, nor can it use the this accessor, which we will introduce soon. Static constructors cannot be explicitly called from programs. They are automatically called by the system, at some time before any instance of the class is created.
    • The that precedes any static member of the reference class.
静态构造函数的例子

下面的代码使用一个静态构造函数来初始化一个名为RandomKey、类型为Random的私有静态字段。Random是由 BCL 提供的产生随机数的类。它在System名称空间中。

`   class RandomNumberClass    {       private static Random RandomKey;         // Private static field

      static RandomNumberClass()               // Static constructor       {          RandomKey = new Random();             // Initialize RandomKey       }

      public int GetRandomNumber()       {          return RandomKey.Next();       }    }

   class Program    {       static void Main()       {          RandomNumberClass a = new RandomNumberClass();          RandomNumberClass b = new RandomNumberClass();

         Console.WriteLine("Next Random #: {0}", a.GetRandomNumber());          Console.WriteLine("Next Random #: {0}", b.GetRandomNumber());       }    }`

该代码的一次执行产生了以下输出:


Next Random #: 47857058 Next Random #: 1124842041


对象初始化器

到目前为止,在本文中,您已经看到对象创建表达式由关键字new后跟一个类构造函数及其参数列表组成。一个对象初始化器通过在表达式末尾放置一个成员初始化列表来扩展语法。对象初始化器允许你在创建一个新的对象实例时设置字段和属性的值。

语法有两种形式,如下所示。一种形式包含构造函数的参数列表,另一种不包含。注意,第一种形式甚至没有使用圆括号来括住参数列表。

                                                  Object initializer                          <ins>↓</ins>    new *TypeName*          { *FieldOrProp* = *InitExpr*, *FieldOrProp* = *InitExpr*, ...}    new *TypeName*(*ArgList*) { <ins>*FieldOrProp = InitExpr*</ins>, <ins>*FieldOrProp = InitExpr*</ins>, ...}                                        ↑                      ↑                                   Member initializer            Member initializer

例如,对于一个名为Point的类,它有两个公共整数字段XY,您可以使用下面的表达式来创建一个新的对象:

   new Point { <ins>X = 5</ins>, <ins>Y = 6</ins> };                  ↑      ↑                 Init X    Init Y

关于对象初始值设定项,需要了解的重要事项如下:

  • The code that creates the object must be able to access the fields and properties being initialized. For example, in the previous code, X and Y must be public.
  • Initialization occurs after the completion of the constructor, so these values may have been set in the constructor and then reset to the same or different values in the object initialization.

下面的代码展示了一个使用对象初始化器的例子。在Main中,pt1只调用构造函数,它设置它的两个字段的值。然而,对于pt2,构造函数将字段的值设置为 1 和2,初始化器将它们更改为56

`   public class Point    {       public int X = 1;       public int Y = 2;    }

   class Program    {       static void Main( )       {                            Object initializer          Point pt1 = new Point();       ↓                 Point pt2 = new Point   { X = 5, Y = 6 };          Console.WriteLine("pt1: {0}, {1}", pt1.X, pt1.Y);          Console.WriteLine("pt2: {0}, {1}", pt2.X, pt2.Y);       }    }`

该代码产生以下输出:


pt1: 1, 2 pt2: 5, 6


析构函数

析构函数在不再引用某个类的实例后执行清理或释放非托管资源所需的操作。非托管资源是使用 Win32 API 获得的文件句柄或非托管内存块。这些东西不是你用就能得到的 .NET 资源,所以如果您坚持使用 .NET 类,你不需要为你的类写析构函数。出于这个原因,我打算把对析构函数的描述留到第二十五章。

只读修饰符

可以用readonly修饰符声明一个字段。其效果类似于将字段声明为const,因为一旦设置了值,就不能更改。

  • Although the const field can only be initialized in the declaration statement of the field, the readonly field can be set in any of the following places:
    • Field declaration statement-similar to const.
    • The constructor of any class. If it is a static field, it must be completed in the static constructor.
  • Although the value of const field must be determined at compile time, the value of readonly field can be determined at run time. This extra freedom allows you to set different values in different environments or in different constructors!
  • Unlike const, it is always like a static one. Here is the real situation of a readonly field:
    • It can be an instance field or a static field.
    • It has a storage location in memory.

例如,下面的代码声明了一个名为Shape的类,有两个readonly字段。

  • The field PI is initialized in its declaration.
  • The field NumberOfSides is set to 3 or 4, depending on which constructor is called.

`   class Shape    {  Keyword           Initialized           ↓                ↓            readonly double PI = 3.1416;       readonly int    NumberOfSides;           ↑                ↑        Keyword            Not initialized

      public Shape(double side1, double side2)                  // Constructor       {          // Shape is a rectangle          NumberOfSides = 4;                ↑          ... Set in constructor       }

      public Shape(double side1, double side2, double side3)    // Constructor       {          // Shape is a triangle          NumberOfSides = 3;                 ↑          ... Set in constructor       }    }`

这个关键字

在类中使用的关键字this是对当前实例的引用。它只能在下列类成员的中使用:

  • Instance constructor.
  • Example tactics.
  • And instance accessors for property indexers. (The indexer will be introduced in the next section. )

显然,由于静态成员不是实例的一部分,所以不能在任何静态函数成员的代码中使用this关键字。相反,它用于以下用途:

  • Classification members and local variables or parameters

  • As an actual parameter

例如,下面的代码声明了类MyClass,带有一个int字段和一个采用单个int参数的方法。方法比较参数和字段的值,并返回较大的值。唯一复杂的因素是字段和形参的名称是相同的:Var1。通过使用this访问关键字来引用字段,这两个名称在方法内部是有区别的。

`   class MyClass {       int Var1 = 10;            ↑    Both are called “Var1”    ↓       public int ReturnMaxSum(int Var1)       {       Parameter     Field                  ↓        ↓              return Var1 > this.Var1                      ? Var1                 // Parameter                      : this.Var1;           // Field       }    }

   class Program {       static void Main()       {          MyClass mc = new MyClass();

         Console.WriteLine("Max: {0}", mc.ReturnMaxSum(30));          Console.WriteLine("Max: {0}", mc.ReturnMaxSum(5));       }    }`

该代码产生以下输出:


Max: 30 Max: 10


索引器

假设你要定义类Employee,有三个类型为string的字段(如图 6-13 中的所示)。然后,您可以使用它们的名称来访问这些字段,如Main中的代码所示。

Image

图 6-13 。没有索引器的简单类

然而,有时候用索引访问它们会很方便,就好像实例是一个字段数组一样。这正是索引器允许你做的事情。如果你要为类Employee写一个索引器,方法Main可能看起来像图 6-14 中的代码。注意,索引器不使用点语法符号,而是使用索引符号,它由方括号之间的索引组成。

Image

图 6-14 。使用索引字段

什么是索引器?

索引器是一对getset访问器,类似于属性的访问器。图 6-15 显示了一个可以获取和设置string类型值的类的索引器的表示。

Image

图 6-15 。索引器的表示

索引器和属性

索引器和属性在很多方面都很相似。

  • Like the property, the indexer does not allocate memory for storage.
  • Indexers and properties are mainly used to provide access to and other data members. These data members are associated with indexers and properties, which provide get and set access.
    • A attribute usually represents a single data member.
    • A indexer usually represents multiple data members.

Image 注意你可以把一个索引器想象成一个属性,它提供对类的多个数据成员的获取和设置访问。您可以通过提供索引来选择许多可能的数据成员中的哪一个,索引本身可以是任何类型,而不仅仅是数字。

关于索引器,您还应该知道以下几点:

  • Like properties, indexers can have one or two accessors.
  • Indexers are always instance members; Therefore, the indexer cannot be declared as static.
  • Like attributes, the code that implements get and set accessors need not be associated with any fields or attributes. The code can do anything, or nothing, as long as the get accessor returns a value of a specified type.
声明一个索引器

声明索引器的语法如下所示。请注意以下关于索引器的内容:

  • An indexer has no name . The name is replaced by the keyword this.
  • The parameter table is between the brackets in .
  • There must be at least one parameter declaration in the parameter list.

             Keyword     Parameter list                 ↓   <ins>        ↓          </ins>    ReturnType this [ *Type param1*, ... ]    {               ↑                  ↑       get     Square bracket   Square bracket       {             ...       }       set       {             ...       }    }

声明索引器类似于声明属性。图 6-16 显示了句法的异同。

Image

图 6-16 。比较索引器声明和属性声明

索引器设置访问器

当索引器是赋值的目标时,set访问器被调用并接收两项数据,如下所示:

  • An implicit parameter named value that holds the data to be stored.
  • One or more index parameters that indicate where it should be stored.

    emp[0] = "Doe";         ↑      ↑        Index   Value     Parameter

您在set访问器中的代码必须检查索引参数,确定数据应该存储在哪里,然后存储它。

图 6-17 显示了set访问器的语法和含义。图的左侧显示了访问器声明的实际语法。右侧显示了访问器的语义,如果它是使用普通方法的语法编写的。右图显示了set访问器具有以下语义:

  • It has a void return type.
  • Use the same parameter list as in the indexer declaration.
  • It has an implicit value parameter named value, which is of the same type as the indexer.

Image

图 6-17 。set 访问器声明的语法和含义

索引器获取访问器

当索引器用于检索值时,使用一个或多个索引参数调用get访问器。索引参数表示要检索的值。

   string s = emp[0];                   ↑               Index parameter

get访问器主体中的代码必须检查索引参数,确定它们代表哪个字段,并返回该字段的值。

图 6-18 显示了get访问器的语法和含义。图的左侧显示了访问器声明的实际语法。右侧显示了访问器的语义,如果它是使用普通方法的语法编写的。get访问器的语义如下:

  • It is the same as the parameter list in the indexer declaration.

Image

图 6-18 。get 访问器声明的语法和含义

关于索引器的更多信息

与属性一样,getset访问器不能被显式调用。相反,当索引器用于值的表达式时,会自动调用get访问器。当用赋值语句给索引器赋值时,会自动调用set访问器。

当“调用”索引器时,参数在方括号之间提供。

     Index   Value        ↓      ↓    emp[0] = "Doe";                               // Calls set accessor    string NewName = emp[0];                      // Calls get accessor                         ↑                       Index

为雇员示例声明索引器

下面的代码为前面的例子声明了一个索引器:class Employee

  • The indexer must read and write the value of type string, so string must be declared as the type of indexer. It must be declared as public so that it can be accessed from outside the class.
  • The three fields in the example are arbitrarily indexed as integers 0 to 2, so the parameter between square brackets (called index in this example) must be of type int.
  • In the body of the set accessor, the code determines which field the index refers to and assigns the value of the implicit variable value to it. In the body of the get accessor, the code determines which field the index refers to and returns the value of that field.

`   class Employee    {       public string LastName;                     // Call this field 0.       public string FirstName;                    // Call this field 1.       public string CityOfBirth;                  // Call this field 2.

      public string this[int index]               // Indexer declaration       {          set                                      // Set accessor declaration          {             switch (index) {                case 0: LastName = value;                   break;                case 1: FirstName = value;                   break;                case 2: CityOfBirth = value;                   break;

               default:                           // (Exceptions in Ch. 11)                   throw new ArgumentOutOfRangeException("index");             }          }

         get                                      // Get accessor declaration          {             switch (index) {                case 0: return LastName;                case 1: return FirstName;                case 2: return CityOfBirth;

               default:                           // (Exceptions in Ch. 11)                   throw new ArgumentOutOfRangeException("index");             }          }       }    }`

另一个索引器例子

以下是索引类Class1的两个int字段的附加示例:

`   class Class1    {       int Temp0;                         // Private field       int Temp1;                         // Private field       public int this [ int index ]      // The indexer       {          get          {             return ( 0 == index )        // Return value of either Temp0 or Temp1                         ? Temp0                         : Temp1;          }

         set          {             if( 0 == index )                Temp0 = value;            // Note the implicit variable "value".             else                Temp1 = value;            // Note the implicit variable "value".          }       }    }

   class Example    {       static void Main()       {          Class1 a = new Class1();

         Console.WriteLine("Values -- T0: {0},  T1: {1}", a[0], a[1]);          a[0] = 15;          a[1] = 20;          Console.WriteLine("Values -- T0: {0}, T1: {1}", a[0], a[1]);       }    }`

该代码产生以下输出:


Values -- T0: 0,  T1: 0 Values -- T0: 15, T1: 20


索引器重载

一个类可以有任意数量的索引器,只要参数列表是不同的;分度器类型不同是不够的。这被称为索引器重载,因为所有索引器都有相同的“名字”——this访问引用。

例如,下面的类有三个索引器:两个类型为string,一个类型为int。在两个string类型的索引器中,一个有一个int参数,另一个有两个int参数。

`   class MyClass    {       public string this [ int index ]       {          get { ... }          set { ... }       }

      public string this [ int index1, int index2 ]       {          get { ... }          set { ... }       }

      public int this [ float index1 ]       {          get { ... }          set { ... }       }

      ...    }`

Image 注意记住一个类的重载索引器必须有不同的参数列表。

访问器上的访问修饰符

在这一章中,你已经看到了两种类型的函数成员有getset访问器:属性和索引器。默认情况下,成员的两个访问者与成员本身具有相同的访问级别。也就是说,如果一个属性的访问级别是public,那么它的两个访问器具有相同的访问级别。索引器也是如此。

但是,您可以为这两个访问者分配不同的访问级别。例如,下面的代码展示了声明私有set访问器和公共get访问器的一个常见且重要的范例。get是公共的,因为属性的访问级别是公共的。

请注意,在这段代码中,虽然可以从类外部读取属性,但它只能从类内部设置,在本例中是由构造函数设置的。这是封装的一个重要工具。

`   class Person     Accessors with different access levels    {                        ↓        ↓            public string Name { get; private set; }       public Person( string name )       {          Name = name;       }    }

   class Program    {       static public void Main( )       {          Person p = new Person( "Capt. Ernest Evans" );          Console.WriteLine( "Person's name is {0}", p.Name );       }    }`

该代码产生以下输出:


Person's name is Capt. Ernest Evans


对于访问者的访问修饰符有一些限制。最重要的如下:

  • Only when a member (attribute or indexer) has both get accessors and set accessors can accessors have access modifiers.
  • Although both accessors must exist, only one of them can have access modifiers.
  • The access modifier of the accessor must be stricter than the access level of the member .

图 6-19 显示了访问级别的层次结构。在图表中,访问者的访问级别必须严格低于成员的访问级别。

例如,如果一个属性的访问级别为public,那么您可以将图表上四个较低的访问级别中的任何一个授予其中一个访问者。但是如果属性的访问级别是protected,那么您可以在其中一个访问器上使用的唯一访问修饰符是private

Image

图 6-19 。严格限制访问器级别的层次结构

分部类和分部类型

一个类的声明可以在几个分部类声明中划分。

  • Each partial classes declaration contains declarations of some class members.
  • The partial classes declaration of a class can be in the same file or in different files.

与单个关键字class相比,每个部分声明必须标记为partial class。除了添加了类型修饰符partial,分部类的声明看起来和普通类的声明一样。

`   Type modifier         ↓      partial class MyPartClass    // Same class name as following      {         member1 declaration         member2 declaration            ...      }

   Type modifier         ↓      partial class MyPartClass    // Same class name as preceding      {         member3 declaration         member4 declaration            ...      }`

Image 注意类型修饰符partial不是一个关键字,所以在其他情况下你可以在你的程序中使用它作为标识符。但是当用在关键字classstructinterface之前时,它表示使用了分部类型。

例如,图 6-20 左边的方框代表一个带有类声明的文件。图中右边的方框表示被分成两个文件的同一个类声明。

Image

图 6-20 。使用分部类型的类拆分

组成一个类的所有分部类声明必须一起编译。使用分部类声明的类与所有类成员都在单个类声明体中声明的意义相同。

Visual Studio 在其标准 Windows 程序模板中使用此功能。当从标准模板创建 ASP.NET 项目、Windows 窗体项目或 Windows Presentation Foundation(WPF)项目时,这些模板会为每个网页、窗体或 WPF 窗口创建两个类文件。在 ASP.NET 或 Windows 窗体的情况下,以下是正确的:

  • A file contains a partial classes containing code generated by Visual Studio, which declares the components on the page. You should not modify the partial classes in this file, because when you modify the component on the page, it will be regenerated by Visual Studio.
  • The other file contains the partial classes that you use to realize the appearance and behavior of the page or form component.

除了分部类之外,还可以创建其他两种分部类型,如下所示:

  • Division structure. (The structure is included in Chapter 10 of . )
  • Partial interface. (Interface is included in Chapter 15 . )

分部分项方法

分部方法是在分部类的不同部分声明的方法。分部方法的不同部分可以在分部类的不同部分中声明,也可以在同一部分中声明。分部方法的两个部分如下:

  • Define partial method declaration
    • Lists the signature and return types.
    • The implementation part of the declaration syntax contains only one semicolon.
  • Implementing partial method declarations
    • Lists the signature and return types.
    • Implementation is in normal format, as you know, it is a statement block.

关于分部方法,需要了解的重要事项如下:

  • The definition and implementation declaration must match in signature and return type. The signature and return type have the following characteristics:
    • The return type must be void.
    • Signature cannot contain access modifier, makes partial method implicitly private .
    • Parameter table cannot contain out parameter.
    • Context partial must be included in the definition and implementation declaration before keyword void.
  • There can be a method to define a division, but no method to implement a division. In this case, the compiler removes the declaration and any calls to the method inside the class. If there is no division method defined, there can be no division method implemented.

下面的代码展示了一个名为PrintSum的分部方法的例子。

  • PrintSum Declared in different parts of partial classes MyClass: the definition statement is in the first part and the implementation statement is in the second part. The implementation prints out the sum of its two integer parameters.
  • Because partial methods are implicitly private, PrintSum cannot be called from outside the class. Method Add is a public method that calls PrintSum.
  • Main Create an object of class MyClass and call public method Add, which calls method PrintSum to print out the sum of input parameters.

`   partial class MyClass    {        Must be void                 ↓       partial void PrintSum(int x, int y);      // Defining partial method         ↑                                ↑    Contextual keyword                   No implementation here       public void Add(int x, int y)       {          PrintSum(x, y);       }    }

   partial class MyClass    {       partial void PrintSum(int x, int y)       // Implementing partial method       {          Console.WriteLine("Sum is {0}", x + y);     ←  Implementation       }    }

   class Program    {       static void Main( )       {          var mc = new MyClass();          mc.Add(5, 6);       }    }`

该代码产生以下输出:


Sum is 11


七、类和继承

类继承

继承允许你定义一个新的类来合并和扩展一个已经声明的类。

  • You can use an existing class, called base class, as the basis of a new class, called derived class. Members of the derived class of consist of the following:
    • Declare one's membership
    • Member of the base class
  • To declare a derived class, you need to add a base class specification after the class name. The base class specification consists of a colon followed by the class name used as the base class. It is said that derived classes inherit directly from the listed base classes.
  • A derived class is considered to be the base class that extends it, because it includes the members of the base class and any additional functions provided in its own declaration.
  • Derived class cannot delete any members inherited by .

例如,下面显示了名为OtherClass的类的声明,该类是从名为SomeClass的类派生而来的:

`                  Class-base specification

                          ↓    class OtherClass : SomeClass        {            ↑     ↑       ...          Colon  Base class    }`

图 7-1 显示了每个类的一个实例。左边的类SomeClass,有一个字段和一个方法。右边的类OtherClass是从SomeClass派生的,包含一个额外的字段和一个额外的方法。

Image

***图 7-1。*基类和派生类

访问继承的成员

对继承成员的访问就像在派生类本身中声明一样。(继承的构造函数有一点不同——我将在本章后面介绍它们。)例如,下面的代码声明了类SomeClassOtherClass,它们显示在图 7-1 中。代码显示,OtherClass的所有四个成员都可以无缝访问,不管它们是在基类还是派生类中声明的。

  • Main Create an object of a derived class OtherClass.
  • The next two lines in Main call Method1 in the base class, use Field1 in the base class, and then use Field2 in the derived class.
  • The next two lines in Main call Method2 in derived from , use Field1 in the base class again, and then use Field2 in the derived class.

`   class SomeClass                          // Base class    {       public string Field1 = "base class field ";       public void Method1( string value ) {          Console.WriteLine("Base class -- Method1:     {0}", value);       }    }

   class OtherClass: SomeClass             // Derived class    {       public string Field2 = "derived class field";       public void Method2( string value ) {          Console.WriteLine("Derived class -- Method2:  {0}", value);       }    }

   class Program    {       static void Main() {          OtherClass oc = new OtherClass();

         oc.Method1( oc.Field1 );       // Base method with base field          oc.Method1( oc.Field2 );       // Base method with derived field          oc.Method2( oc.Field1 );       // Derived method with base field          oc.Method2( oc.Field2 );       // Derived method with derived field       }    }`

该代码产生以下输出:


Base class -- Method1:     base class field Base class -- Method1:     derived class field Derived class -- Method2:  base class field Derived class -- Method2:  derived class field


所有的类都是从类对象派生出来的

除了特殊类object,所有的类都是派生类,即使它们没有一个基于类的规范。类object是唯一没有被派生的类,因为它是继承层次的基础。

没有基类规范的类是直接从类object隐式派生的。省略基类规范只是指定object是基类的简写。两种形式语义等价,如图图 7-2 所示。

Image

***图 7-2。*左边的类声明隐式派生自类对象,而右边的类声明显式派生自对象。这两种形式在语义上是等价的。

关于类派生的其他重要事实如下:

  • A class declares that only one class can be listed in its base class specification. This is called single inheritance .
  • Although a class can only directly inherit a base class, there is no restriction on the derived level . That is to say, the class listed as the base class may be derived from another class, and another class is derived from another class, and so on, until finally reaching object.

基类派生类是相对术语。所有的类都是派生类,要么来自object要么来自另一个类——所以通常当我们称一个类为派生类时,我们的意思是它是直接从除了object之外的某个类派生的。图 7-3 显示了一个简单的类层次结构。在这之后,我不会在图中显示object,因为所有的类最终都是从它派生的。

Image

***图 7-3。*一个阶级等级体系

屏蔽基类的成员

派生类不能删除它继承的任何成员;但是,它可以用同名的成员来屏蔽基类成员。这非常有用,也是继承的主要特征之一。

例如,您可能希望从具有特定方法的基类继承。尽管该方法对于声明它的类来说是完美的,但它可能并不完全符合您在派生类中的要求。在这种情况下,您要做的是用派生类中声明的新成员来屏蔽基类方法。屏蔽派生类中的基类成员的一些重要方面如下:

  • Mask a member that inherits data, and declare a new member with the same type and the same name .
  • To mask an inherited function member, declare a new function member with the same signature. Remember, the signature consists of a name and a list of parameters, but does not include the return type.
  • To let the compiler know that you intentionally blocked an inherited member, use the new modifier. Without it, the program can compile successfully, but the compiler will warn you that an inherited member is hidden.
  • You can also block static members.

下面的代码声明了一个基类和一个派生类,每个基类都有一个名为Field1string成员。关键字new用于明确告诉编译器屏蔽基类成员。图 7-4 展示了每个类的一个实例。

`   class SomeClass                        // Base class    {       public string Field1;       ...    }

   class OtherClass : SomeClass           // Derived class    {       new public string Field1;           // Mask base member with same name        ↑      Keyword` Image

***图 7-4。*屏蔽一个基类的字段

在下面的代码中,OtherClassSomeClass派生,但是隐藏了它的两个继承成员。注意new修改器的使用。图 7-5 说明了代码。

`   class SomeClass                                      // Base class    {       public string Field1 = "SomeClass Field1";       public void   Method1(string value)           { Console.WriteLine("SomeClass.Method1:  {0}", value); }    }

   class OtherClass : SomeClass                        // Derived class    {  Keyword        ↓       new public string Field1 = "OtherClass Field1";  // Mask the base member.       new public void   Method1(string value)          // Mask the base member.        ↑   { Console.WriteLine("OtherClass.Method1:  {0}", value); }    }  Keyword

   class Program    {       static void Main()       {          OtherClass oc = new OtherClass();       // Use the masking member.          oc.Method1(oc.Field1);                  // Use the masking member.       }    }`

该代码产生以下输出:


OtherClass.Method1:  OtherClass Field1


Image

**图 7-5。**隐藏基类的一个字段和一个方法

基地通道

如果你的派生类必须访问一个隐藏的继承成员,你可以通过使用一个基本访问表达式来访问它。该表达式由关键字base组成,后跟一个句点和成员名称,如下所示:

   Console.WriteLine("{0}", <ins>base.Field1</ins>);                                  ↑                                                                      ↑                                      Base access

例如,在下面的代码中,派生类OtherClassField1隐藏在其基类中,但是通过使用基本访问表达式来访问它。

`   class SomeClass {                                       // Base class       public string Field1 = "Field1 -- In the base class";    }

   class OtherClass : SomeClass {                          // Derived class

      new public string Field1 = "Field1 -- In the derived class";        ↑                   ↑       Hides the field in the base class       public void PrintField1()       {          Console.WriteLine(Field1);              // Access the derived class.          Console.WriteLine(base.Field1);         // Access the base class.       }                         ↑              }                         Base access

   class Program {       static void Main()       {          OtherClass oc = new OtherClass();          oc.PrintField1();       }    }`

该代码产生以下输出:


Field1 -- In the derived class Field1 -- In the base class


如果您发现您的程序代码经常使用该功能,即访问隐藏的继承成员,您可能需要重新评估您的类的设计。一般来说,有更优雅的设计——但如果有其他东西都不行的情况,这个功能就在那里。

使用对基类的引用

派生类的实例由基类的实例加上派生类的附加成员组成。对派生类的引用指向整个类对象,包括基类部分。

如果你有一个对派生类对象的引用,你可以通过使用转换操作符将引用转换为基类的类型来得到一个对该对象基类部分的引用。转换操作符放在对象引用的前面,由一组括号组成,括号中包含被转换到的类的名称。铸造在第十六章中有详细介绍。

接下来的几节将介绍如何通过引用对象的基类部分来访问对象。我们将从下面的两行代码开始,它们声明了对对象的引用。图 7-6 说明了代码,并显示了不同变量所看到的对象部分。

  • The first line declares and initializes the variable derived, which contains a reference to an object of type MyDerivedClass.
  • The second line declares a variable MyBaseClass of the base class type, converts the reference in derived into this type, and gives a reference to the base class part of the object.
    • The reference to the base class part is stored in the variable mybc to the left of the assignment operator.
    • The reference to the base class part cannot "see" the rest of the derived class object, because it "sees" it through the reference to the base class.

   MyDerivedClass derived = new MyDerivedClass();      // Create an object.    MyBaseClass mybc       = (MyBaseClass) derived;     // Cast the reference. Image

图 7-6 。Reference derived 可以看到整个 MyDerivedClass 对象,而 mybc 只能看到对象的 MyBaseClass 部分。

下面的代码显示了这两个类的声明和使用。图 7-7 说明了内存中的对象和引用。

Main创建一个类型为MyDerivedClass的对象,并将其引用存储在变量derived中。Main还创建了一个MyBaseClass类型的变量,并用它来存储对对象基类部分的引用。当在每个引用上调用Print方法时,调用调用引用可以看到的方法的实现,产生不同的输出字符串。

`   class MyBaseClass    {       public void Print()       {          Console.WriteLine("This is the base class.");       }    }

   class MyDerivedClass : MyBaseClass    {       new public void Print()       {          Console.WriteLine("This is the derived class.");       }    }

   class Program    {       static void Main()       {          MyDerivedClass derived = new MyDerivedClass();          MyBaseClass mybc = (MyBaseClass)derived;                                   ↑                             Cast to base class          derived.Print();           // Call Print from derived portion.          mybc.Print();              // Call Print from base portion.       }    }`

该代码产生以下输出:


This is the derived class. This is the base class.


Image

***图 7-7。*对派生类和基类的引用

虚拟和覆盖方法

在上一节中,您看到了当您通过使用对基类的引用来访问派生类的对象时,您只能获得基类的成员。虚拟方法允许对基类的引用“向上”访问派生类。

如果满足以下条件,您可以使用对基类的引用来调用派生类中的方法:

  • Methods in derived classes and methods in base classes each have the same signature and return type.
  • The method in the base class is labeled virtual.
  • The method in the derived class is labeled override.

例如,以下代码显示了基类和派生类中方法的virtualoverride修饰符:

   class MyBaseClass                                   // Base class    {       <ins>virtual</ins> public void Print()          ↑        ...    class MyDerivedClass : MyBaseClass                  // Derived class    {       <ins>override</ins> public void Print()          ↑

图 7-8 说明了这组virtualoverride方法。注意这种行为与前一种情况有什么不同,在前一种情况下,我使用了new来隐藏基类成员。

  • When the Print method is called with a reference to the base class (mybc), the method call is passed up to the derived class and executed because
    • The method in the base class is marked as virtual.
    • There is a matching override method in the derived class. illustrates this point by displaying the arrow coming out from behind the virtual Print method and pointing to the override Print method.

Image

***图 7-8。*一个虚拟方法和一个覆盖方法

下面的代码与上一节中的相同,但是这一次,方法被标记为virtualoverride。这会产生一个与前一个示例非常不同的结果。在此版本中,通过基类调用方法会调用派生类中的方法。

`   class MyBaseClass    {       virtual public void Print()       {          Console.WriteLine("This is the base class.");       }    }

   class MyDerivedClass : MyBaseClass    {       override public void Print()       {          Console.WriteLine("This is the derived class.");       }    }

   class Program    {       static void Main()       {          MyDerivedClass derived = new MyDerivedClass();          MyBaseClass mybc       = (MyBaseClass)derived;                                         ↑          derived.Print();          Cast to base class          mybc.Print();       }    }`

该代码产生以下输出:


This is the derived class. This is the derived class.


关于virtualoverride修改器的其他重要信息如下:

  • Overrides and overridden methods must have the same accessibility. In other words, the covered method cannot be, for example, private and the covered method public.
  • Cannot override static or there is no method declared as virtual.
  • Methods, properties and indexers (which I introduced in the previous chapter), and another member type, called event (which I will introduce later in the text), can be declared as virtual and override.
覆盖标记为 override 的方法

重写方法可以发生在任何级别的继承之间。

  • When you call an overridden method with a reference to the base class part of an object, the method call is passed up to the derivation hierarchy to be executed to the most derived version of the method marked override.
  • If there are other method declarations not marked as override at a higher level of derivation, they will not be called.

例如,下面的代码显示了构成继承层次结构的三个类:MyBaseClassMyDerivedClassSecondDerived。这三个类都包含一个名为Print的方法,具有相同的签名。在MyBaseClass中,Print被标注为virtual。在MyDerivedClass中,标注为override。在类SecondDerived中,你可以用overridenew来声明方法Print。让我们看看在每种情况下会发生什么。

`   class MyBaseClass                                    // Base class    {       virtual public void Print()       { Console.WriteLine("This is the base class."); }    }

   class MyDerivedClass : MyBaseClass                   // Derived class    {       override public void Print()       { Console.WriteLine("This is the derived class."); }    }

   class SecondDerived : MyDerivedClass                 // Most-derived class    {       ... // Given in the following pages    }`

案例 1:用覆盖声明打印

如果你将SecondDerivedPrint方法声明为override,那么它将覆盖方法的两个派生较少的版本,如图图 7-9 所示。如果对基类的引用被用来调用Print,它会沿着链一直传递到类SecondDerived中的实现。

下面的代码实现了这种情况。注意方法Main最后两行中的代码。

  • The first of the two statements calls the Print method by using a reference to the highest-level derived class SecondDerived. This is not called by reference to the base class part, so it will call the method implemented in SecondDerived.
  • However, the second statement calls the Print method by referring to the base class MyBaseClass.

`   class SecondDerived : MyDerivedClass    {       override public void Print() {          ↑   Console.WriteLine("This is the second derived class.");       }    }

   class Program    {       static void Main()       {          SecondDerived derived = new SecondDerived(); // Use SecondDerived.          MyBaseClass mybc = (MyBaseClass)derived;     // Use MyBaseClass.

         derived.Print();          mybc.Print();       }    }`

结果是不管Print是通过派生类还是基类调用,最具派生类的方法都被调用。当通过基类调用时,它会沿着继承层次向上传递。该代码产生以下输出:


This is the second derived class. This is the second derived class.


Image

***图 7-9。*执行被传递到多级覆盖链的顶端。

案例 2:用 new 声明打印

如果改为将SecondDerivedPrint方法声明为new,结果如图图 7-10 所示。Main与前一种情况相同。

`   class SecondDerived : MyDerivedClass    {       new public void Print()       {          Console.WriteLine("This is the second derived class.");       }    }

   class Program    {       static void Main()                                    // Main       {          SecondDerived derived = new SecondDerived();       // Use SecondDerived.          MyBaseClass mybc      = (MyBaseClass)derived;      // Use MyBaseClass.

         derived.Print();          mybc.Print();       }    }`

结果是,当通过对SecondDerived的引用调用方法Print时,SecondDerived中的方法被执行,正如您所料。然而,当通过对MyBaseClass的引用调用该方法时,该方法调用只向上传递一级,到达类MyDerived,在那里执行。这两种情况的唯一区别是SecondDerived中的方法是用修饰符override还是修饰符new声明的。

该代码产生以下输出:


This is the second derived class. This is the derived class.


Image

***图 7-10。*隐藏被覆盖的方法

覆盖其他成员类型

在前面的几节中,您已经看到了virtual / override名称是如何在方法上工作的。这些与属性、事件和索引器的工作方式完全相同。例如,下面的代码使用virtual / override显示了一个名为MyProperty的只读属性。

`   class MyBaseClass    {       private int _myInt = 5;       virtual public int MyProperty       {          get { return _myInt; }       }    }

   class MyDerivedClass : MyBaseClass    {       private int _myInt = 10;       override public int MyProperty       {          get { return _myInt; }       }    }

   class Program    {       static void Main()       {          MyDerivedClass derived = new MyDerivedClass();          MyBaseClass mybc       = (MyBaseClass)derived;

         Console.WriteLine( derived.MyProperty );          Console.WriteLine( mybc.MyProperty );       }    }`

该代码产生以下输出:


10 10


构造函数执行

在前一章中,你看到了一个构造函数执行代码来准备一个类供使用。这包括初始化类的静态和实例成员。在这一章中,你看到了派生类对象的一部分是基类的一个对象。

  • In order to create the base class part of the object, the constructor of the base class is implicitly called as part of the instance creation process.
  • Each class in the inheritance hierarchy chain executes its own base class constructor before executing its own constructor body.

例如,下面的代码显示了类MyDerivedClass及其构造函数的声明。当调用构造函数时,它在执行自己的主体之前调用无参数构造函数MyBaseClass()

   class MyDerivedClass : MyBaseClass    {       MyDerivedClass()        // Constructor uses base constructor MyBaseClass()       {          ...       }

图 7-11 显示了施工顺序。创建实例时,首先要做的事情之一是初始化对象的所有实例成员。之后,基类构造函数被调用。只有这样,类本身的构造函数体才会被执行。

Image

***图 7-11。*宾语结构的顺序

例如,在下面的代码中,在基类构造函数被调用之前,MyField1MyField2的值将被分别设置为50

`   class MyDerivedClass : MyBaseClass    {       int MyField1 = 5;                      // 1. Member initialized       int MyField2;                          //    Member initialized

      public MyDerivedClass()                // 3. Body of constructor executed       {          ...       }    }

   class MyBaseClass    {       public MyBaseClass()                   // 2. Base class constructor called       {          ...       }    }`

Image 小心在构造函数中调用虚方法强烈不鼓励。当执行基类构造函数时,基类中的虚方法将调用派生类中的重写方法。但那是在派生构造函数的主体被执行之前。因此,它会在类完全初始化之前向上调用派生类。

构造函数初始值设定项

默认情况下,构造对象时会调用基类的无参数构造函数。但是构造函数可以重载,所以一个基类可能不止一个。如果你想让你的派生类使用一个特定的基类构造函数而不是无参数构造函数,你必须在一个构造函数初始化器中指定它。

有两种形式的构造函数初始值设定项:

  • The first form uses the keyword base and specifies which base class constructor to use.
  • The second form uses the keyword this and specifies which constructor in the class should be used.

基类构造函数初始值设定项放在类的构造函数声明中参数列表后面的冒号后面。构造函数初始化器由关键字base和要调用的基构造函数的参数列表组成。

例如,下面的代码显示了类MyDerivedClass的构造函数。

  • The constructor initializer specifies that the construction process calls the base class constructor with two parameters, where the first parameter is a string and the second parameter is a int.
  • The parameters in the basic parameter table must match the parameter table of expected basic constructor in type and order.

Constructor initializer                                               <ins>     ↓     </ins>    public MyDerivedClass( int x, string s ) : <ins>base</ins>( s, x )    {                                           ↑    ...                                      Keyword

当你在没有构造函数初始化器的情况下声明一个构造函数时,这是一个带有由base()组成的构造函数初始化器的表单的快捷方式,如图图 7-12 所示。这两种形式在语义上是等价的。

Image

***图 7-12。*建造师的等价形式

另一种形式的构造函数初始化器指示构造过程(实际上是编译器)使用来自同一个类的不同构造函数。例如,下面显示了类MyClass的单参数构造函数。但是,这个单参数构造函数使用了来自同一个类的构造函数,但是有两个参数,提供一个默认参数作为第二个参数。

                                   Constructor initializer                           <ins>               ↓              </ins>    public MyClass(int x): <ins>this</ins>(x, "Using Default String")    {                       ↑       ...               Keyword    }

另一种特别方便的情况是,一个类有几个构造函数,它们有公共代码,应该总是在对象构造过程的开始执行。在这种情况下,您可以提取公共代码,并将其放在一个构造函数中,该构造函数被所有其他构造函数用作构造函数初始值设定项。事实上,这是一个建议的实践,因为它减少了代码重复。

你可能认为你可以声明另一个方法来执行这些普通的初始化,并让所有的构造函数调用这个方法。这不是很好,有几个原因。首先,当编译器知道一个方法是构造函数时,它可以优化某些东西。第二,有些事情只能在构造函数中完成,而不能在其他地方完成。例如,在前一章中,你了解到readonly字段只能在构造函数中初始化。如果您试图在任何其他方法中初始化一个readonly字段,您将得到一个编译器错误,即使该方法仅由一个构造函数调用。

回到那个公共构造函数——如果它可以独立作为一个有效的构造函数,初始化需要初始化的类中的所有东西,那么让它作为一个public构造函数是非常好的。

然而,如果它没有完全初始化一个对象呢?在这种情况下,不允许从类外部调用构造函数,因为这样会创建未完全初始化的对象。为了避免这个问题,您可以声明构造函数private而不是public,并且只让其他构造函数使用它。以下代码说明了这种用法:

`   class MyClass    {       readonly int    firstVar;       readonly double secondVar;

      public string UserName;       public int UserIdNumber;

      private MyClass( )            // Private constructor performs initializations       {                             // common to the other constructors          firstVar  = 20;          secondVar = 30.5;       }

      public MyClass( string firstName ) : this() // Use constructor initializer       {          UserName     = firstName;          UserIdNumber = -1;       }

      public MyClass( int idNumber ) : this( )    // Use constructor initializer       {          UserName     = "Anonymous";          UserIdNumber = idNumber;       }    }`

类访问修饰符

一个类可以被系统中的其他类看到和访问。本节解释了类的可访问性。虽然我将在解释和例子中使用类,因为这是我到目前为止在本文中所涉及的,但可访问性规则也适用于我稍后将涉及的其他类型。

术语可见有时也用于术语可达。它们可以互换使用。类的可访问性有两个级别:publicinternal

  • The class marked public can be accessed by the code in any assembly in the system. To make a class visible to other assemblies, use the public access modifier, as shown below:   Keyword       ↓    public class MyBaseClass    { ...
  • A class marked internal can only be seen by classes in its own assembly. (Remember in Chapter 1 that a assembly is either a program or a DLL. I will introduce the components in detail in Chapter 21 . )     Keyword       ↓    internal class MyBaseClass    { ...
    • This is the default accessibility level, so code outside the assembly cannot access the class unless the modifier public is explicitly specified in the class declaration. You can explicitly declare an internal class by using the internal access modifier.

图 7-13 说明了从组件外部对internalpublic类的访问。类MyClass对于左边组件中的类是不可见的,因为MyClass被标记为internal。然而,类OtherClass对于左边的类是可见的,因为它被标记为public

Image

***图 7-13。*其他程序集中的类可以访问公共类,但不能访问内部类。

程序集之间的继承

到目前为止,我一直在包含基类的同一个程序集中声明派生类。但是 C# 也允许你从不同程序集中定义的基类派生一个类。

若要使您的类从另一个程序集中的基类派生,必须满足以下条件:

  • The base class must be declared public so that it can be accessed from outside its assembly.
  • You must include a reference to the assembly that contains the base class in the References section of the Visual Studio project. You can find the title in Solution Explorer.

为了在不使用完全限定名的情况下更容易引用另一个程序集中的类和类型,请在源文件的顶部放置一个using指令,其命名空间包含您想要访问的类或类型。

Image 注意添加对另一个程序集的引用和添加一个using指令是两件不同的事情。添加对另一个程序集的引用会告诉编译器所需类型的定义位置。添加using指令允许您引用其他类,而不必使用它们的完全限定名。第二十一章对此有详细介绍。

例如,下面两段来自不同程序集中的代码显示了从另一个程序集中继承一个类是多么容易。第一个代码清单创建了一个程序集,它包含一个名为MyBaseClass的类的声明,该类具有以下特征:

  • It is declared in a source file named Assembly1.cs and a namespace named BaseClassNS.
  • It is declared as public so that it can be accessed from other assemblies.
  • It contains a single member, a method named PrintMe, and it just writes a simple message to identify the class.

   // Source file name Assembly1.cs    using System;        Namespace containing declaration of base class                  ↓    namespace BaseClassNS    { Declare the class public so it can be seen outside the assembly.         ↓       public class MyBaseClass {          public void PrintMe() {             Console.WriteLine("I am MyBaseClass");          }       }    }

第二个程序集包含一个名为DerivedClass的类的声明,该类继承自第一个程序集中声明的MyBaseClass。源文件名为Assembly2.cs。图 7-14 展示了两个组件。

  • DerivedClass has an empty body, but it inherits the method PrintMe from MyBaseClass.
  • Main Create an object of type DerivedClass and call its inheritance method PrintMe.

`   // Source file name Assembly2.cs    using System;    using BaseClassNS;              ↑    Namespace containing declaration of base class    namespace UsesBaseClass    {                  Base class in other assembly                              ↓       class DerivedClass: MyBaseClass {          // Empty body       }

      class Program {          static void Main( )          {             DerivedClass mdc = new DerivedClass();             mdc.PrintMe();          }       }    }`

该代码产生以下输出:


I am MyBaseClass


Image

***图 7-14。*跨程序集继承

成员访问修饰符

前两节解释了类的可访问性。对于类可访问性,只有两个修饰符— internalpublic。本节涵盖了成员可访问性。类可访问性描述了类的可见性;成员可访问性描述了类对象成员的可见性。

类中声明的每个成员对系统的各个部分都是可见的,这取决于在类声明中分配给它的访问修饰符。您已经看到了private成员仅对同一类的其他成员可见,而public成员对程序集之外的类也是可见的。在本节中,我们将再次查看publicprivate访问级别,以及其他三个可访问级别。

在研究成员可访问性的细节之前,我需要先提一些一般性的事情:

在一个类的声明中显式声明的所有成员对彼此都是可见的,不管它们的可访问性规范如何。继承的成员没有在类的声明中显式声明,所以,正如你将看到的,继承的成员对派生类的成员可能是可见的,也可能是不可见的。* The following are the names of the five member access levels. So far, I have only introduced public and private.

*   `public`
*   `private`
*   `protected`
*   `internal`
*   `protected internal`*   You must specify a member access level for each member. If no access level is specified for the member, its implicit access level is `private`.*   A member cannot be more accessible than its class. That is to say, if the accessibility level of a class limits it to the assembly, the individual members of the class can't be seen outside the assembly, no matter what their access modifiers are, even `public`.
访问成员的区域

一个类通过用访问修饰符标记它的成员来指定它的哪些成员可以被其他类访问。你已经看到了publicprivate修改器。下面的声明显示了一个类,该类声明了具有五种访问级别的成员:

   public class MyClass    {       public             int Member1;       private            int Member2;       protected          int Member3;       internal           int Member4;       protected internal int Member5;       ...

另一个类——比方说 classB——可以或不可以根据它的两个特征访问这些成员,这两个特征是:

  • Is class b from MyClass
  • Is the derived class B the same as MyClass

类在同一个程序集中

这两个特征产生了四个组,如图 7-15 所示。与类别MyClass相关,另一个类别可以是以下任一类别:

  • In the same assembly, from MyClass in the same assembly (bottom right)
  • Derived from, but not from MyClass in different assemblies (lower left)
  • Derived from MyClass in different assemblies (upper right)
  • Derived from, not from MyClass (top left)

派生

这些特征用于定义五个访问级别,我将在下一节中介绍。

Image

***图 7-15。*无障碍区域

公共成员可访问性

public访问级别限制最少。程序集内外的所有类都可以自由访问该成员。图 7-16 展示了MyClasspublic类成员的可访问性。

要声明一个公共成员,使用public访问修饰符,如下所示。

`   Keyword       ↓

   public int Member1;` Image

***图 7-16。*公共类的公共成员对同一程序集和其他程序集中的所有类都是可见的。

私人会员可访问性

private访问级别是最严格的。

  • private Class members can only be accessed by members of their own class. It cannot be accessed by other classes, including classes derived from it.
  • However, members of private can be accessed by members of classes nested in their classes. Nested classes are contained in in Chapter 25.

图 7-17 展示了一个private成员的可访问性。

Image

***图 7-17。*任何类的私有成员仅对它自己的类(或嵌套类)的成员可见。

受保护成员的可访问性

protected访问级别类似于private访问级别,除了它也允许从类派生的类访问成员。图 7-18 说明了protected的可达性。请注意,即使程序集之外从类派生的类也可以访问该成员。

Image

***图 7-18。*公共类的受保护成员对它自己的类和从它派生的类的成员是可见的。派生类甚至可以位于其他程序集中。

内部成员可访问性

标记为internal的成员对集合中的所有类可见,但对集合外的类不可见,如图 7-19 中的所示。

Image

***图 7-19。*公共类的内部成员对同一程序集内的任何类的成员都是可见的,但对程序集外的类是不可见的。

受保护的内部成员可访问性

标记为protected internal的成员对从该类继承的所有类可见,也对程序集内的所有类可见,如图 7-20 中的所示。注意,允许访问的类集合是由protected修饰符允许的类集合加上由internal修饰符允许的类集合的组合。注意,这是protectedinternal联合——而不是交集。

Image

***图 7-20。*公共类的受保护内部成员对同一程序集中的类成员以及从该类派生的类成员可见。对于不是从该类派生的其他程序集中的类,它是不可见的。

成员访问修饰符概要

下面两个表总结了五个成员访问级别的特征。表 7-1 列出了每个修改器,并给出了其效果的直观总结。

Image

图 7-21 显示了五个成员访问修饰符的相对可访问性。

Image

***图 7-21。*各种成员访问修饰符的相对可访问性

表 7-2 在表的左侧列出了访问修饰符,在顶部列出了类的类别。派生类是指从声明成员的类派生的类。非派生的意味着不是从声明成员的类派生的类。单元格中的复选标记意味着类的类别可以访问带有相应修饰符的成员。

Image

抽象成员

抽象成员是被设计为被覆盖的函数成员。抽象成员具有以下特征:

  • It must be a function member. That is, fields and constants cannot be abstract members.
  • Must be marked with abstract modifier.
  • There is no code block that can be implemented. The code of an abstract member is represented by a semicolon.

例如,类定义中的以下代码声明了两个抽象成员:一个名为PrintStuff的抽象方法和一个名为MyProperty的抽象属性。注意分号代替了实现块。

`    Keyword                                            Semicolon in place of implementation       ↓                                    ↓    abstract public void PrintStuff(string s);

   abstract public int MyProperty    {       get;  ←  Semicolon in place of implementation       set;  ←  Semicolon in place of implementation    }`

抽象成员只能在抽象类中声明,我们将在下一节中讨论。可以将四种类型的成员声明为抽象成员:

  • way
  • attribute
  • event
  • Indexer

关于抽象成员的其他重要事实如下:

  • Abstract members, although they must be covered by the corresponding members in the derived class, cannot use virtual modifier in addition to abstract modifier .
  • Like virtual members, the implementation of abstract members in derived classes must specify the override modifier.

表 7-3 比较对比虚拟成员和抽象成员。

Image

抽象类

抽象类被设计为从。一个抽象类只能作为另一个类的基类。

  • You can't create an instance of an abstract class.
  • Declare an abstract class with the abstract modifier.

    Keyword       ↓    abstract class MyClass    {       ...    }

  • An abstract class can contain abstract members or regular non-abstract members. Members of abstract classes can be any combination of abstract members and ordinary members and implementations.
  • An abstract class itself can be derived from another abstract class. For example, the following code shows an abstract class derived from another abstract class:

`   abstract class AbClass                    // Abstract class    {       ...    }

   abstract class MyAbClass : AbClass        // Abstract class derived from    {                                         // an abstract class       ...    }`

  • Any class derived from an abstract class must use the override keyword to implement all abstract members of the class, unless the derived class itself is abstract.
抽象类和抽象方法的例子

下面的代码展示了一个名为AbClass的抽象类,它有两个方法。

第一个方法是一个普通的方法,它有一个输出类名的实现。第二种方法是必须在派生类中实现的抽象方法。类DerivedClass继承自AbClass并实现和覆盖抽象方法。Main创建一个DerivedClass的对象并调用它的两个方法。

`    Keyword       ↓    abstract class AbClass                                  // Abstract class    {       public void IdentifyBase()                           // Normal method       { Console.WriteLine("I am AbClass"); }        Keyword          ↓       abstract public void IdentifyDerived();              // Abstract method    }

   class DerivedClass : AbClass                            // Derived class    {   Keyword           ↓       override public void IdentifyDerived()               // Implementation of       { Console.WriteLine("I am DerivedClass"); }          // abstract method    }

   class Program    {       static void Main()       {          // AbClass a = new AbClass();        // Error.  Cannot instantiate          // a.IdentifyDerived();              // an abstract class.

         DerivedClass b = new DerivedClass(); // Instantiate the derived class.          b.IdentifyBase();                    // Call the inherited method.          b.IdentifyDerived();                 // Call the "abstract" method.       }    }`

该代码产生以下输出:


I am AbClass I am DerivedClass


抽象类的另一个例子

下面的代码显示了包含数据成员和函数成员的抽象类的声明。记住,数据成员——字段和常量——不能声明为abstract

`   abstract class MyBase     // Combination of abstract and nonabstract members    {       public int SideLength        = 10;             // Data member       const  int TriangleSideCount = 3;              // Data member

      abstract public void PrintStuff( string s );   // Abstract method       abstract public int  MyInt { get; set; }       // Abstract property

      public int PerimeterLength( )                  // Regular, nonabstract method       { return TriangleSideCount * SideLength; }    }

   class MyClass : MyBase    {       public override void PrintStuff( string s )    // Override abstract method       { Console.WriteLine( s ); }

      private int _myInt;       public override int MyInt                      // Override abstract property       {          get { return _myInt; }          set { _myInt = value; }       }    }

   class Program    {       static void Main( string[] args )       {          MyClass mc = new MyClass( );          mc.PrintStuff( "This is a string." );          mc.MyInt = 28;          Console.WriteLine( mc.MyInt );          Console.WriteLine( "Perimeter Length: {0}", mc.PerimeterLength( ) );       }    }`

该代码产生以下输出:


This is a string. 28 Perimeter Length: 30


密封类

在上一节中,您看到了抽象类必须用作基类——它不能被实例化为独立的类对象。一个密封类的情况正好相反。

  • Sealed classes can only be instantiated as independent class objects, not as base classes.
  • A sealed class is marked with the sealed modifier.

例如,下面的类是一个密封类。任何将它作为另一个类的基类的尝试都会产生编译错误。

   Keyword             ↓    sealed class MyClass    {       ...    }

静态类

静态类是所有成员都是静态的类。静态类用于对不受实例数据影响的数据和函数进行分组。静态类的一个常见用途可能是创建一个包含数学方法和值的数学库。

关于静态类,需要知道的重要事情如下:

  • The class itself must be marked with static.
  • All members of the class must be static.
  • A class can have a static constructor, but it cannot have an instance constructor, because an instance of a class cannot be created.
  • Static classes are implicitly sealed. That is, you cannot inherit from a static class.

通过使用类名和成员名,可以像访问任何静态成员一样访问静态类的成员。

下面的代码显示了一个静态类的示例:

`   Class must be marked static       ↓    static public class MyMath    {       public static float PI = 3.14f;       public static bool IsOdd(int x)                 ↑      { return x % 2 == 1; }             Members must be static                 ↓            public static int Times2(int x)                       { return 2 * x; }    }

   class Program    {       static void Main( )       {                                           Use class name and member name.          int val = 3;                                        ↓               Console.WriteLine("{0} is odd is {1}.", val,  MyMath.IsOdd(val));          Console.WriteLine("{0} * 2 = {1}.",     val,  MyMath.Times2(val));       }    }`

该代码产生以下输出:


3 is odd is True. 3 * 2 = 6.


扩展方法

到目前为止,在本文中,你看到的每一个方法都与声明它的类相关联。扩展方法特性扩展了这个界限,允许你编写与类相关的方法,而不是声明它们的类。

要了解如何使用这个特性,请看下面的代码。它包含类MyData,存储三个类型为double的值,还包含一个构造函数和一个名为Sum的方法,返回三个存储值的总和。

`   class MyData    {       private double D1;                                     // Fields       private double D2;       private double D3;

      public MyData(double d1, double d2, double d3)         // Constructor       {          D1 = d1; D2 = d2; D3 = d3;       }

      public double Sum()                                    // Method Sum       {          return D1 + D2 + D3;       }    }`

这是一个非常有限的类,但是假设它包含另一个方法,返回三个数据点的平均值,那么它会更有用。根据您目前对类的了解,有几种方法可以实现附加功能:

  • If you have source code and can modify this class, of course, you can also add new methods to this class.
  • However, if you can't modify this class-for example, if it is in a third-party class library-then, as long as it is not sealed, you can treat it as a base class and implement additional methods in classes derived from it.

但是,如果您没有访问代码的权限,或者该类是密封的,或者有一些其他设计原因阻止了这些解决方案的工作,那么您将不得不在另一个类中编写一个方法,该方法使用该类的公共可用成员。

例如,您可以编写一个类似下面代码中的类。代码包含一个名为ExtendMyData的静态类,该类包含一个名为Average的静态方法,该方法实现了附加功能。注意,该方法将MyData的一个实例作为参数。

`   static class ExtendMyData         Instance of MyData class    {                                    ↓           public static double Average( MyData md )       {          return md.Sum() / 3;       }         ↑    }   Use the instance of MyData.

   class Program    {       static void Main()       {                                                   Instance of MyData          MyData md = new MyData(3, 4, 5);                        ↓          Console.WriteLine("Average: {0}", ExtendMyData.Average(md));       }                                                 ↑      }                                             Call the static method.`

该代码产生以下输出:


Average: 4


虽然这是一个完美的解决方案,但是如果您可以在类实例本身上调用该方法,而不是创建另一个类的实例来操作它,那将会更好。下面两行代码说明了不同之处。第一个使用刚刚展示的方法——在另一个类的实例上调用静态方法。第二个展示了我们想要使用的形式——调用对象本身的实例方法。

   ExtendMyData.Average( md )               // Static invocation form    md.Average();                            // Instance invocation form

扩展方法允许您使用第二种形式,即使第一种形式是编写调用的正常方式。

通过对方法Average的声明做一点小小的修改,您可以使用实例调用的形式。您需要做的更改是在参数声明中的类型名称前添加关键字this,如下所示。将this关键字添加到静态类的静态方法的第一个参数会将其从类ExtendMyData的常规方法更改为类MyData扩展方法。现在,您可以使用这两种调用形式。

  Must be a static class       ↓        static class ExtendMyData    {   Must be public and static                           Keyword and type         <ins>    ↓     </ins>                   <ins>    ↓     </ins>        public static double Average( this MyData md )       {          ...       }    }

扩展方法的重要要求如下:

  • Classes that declare extension methods must declare static.
  • The extension method itself must declare static.
  • The extension method must contain the keyword this as its first parameter type, followed by the class name it is extending.

图 7-22 展示了一个扩展方法的结构。

Image

***图 7-22。*一个扩展方法的结构

下面的代码展示了一个完整的程序,包括类MyData和扩展方法Average,在类ExtendMyData中声明。注意方法Average被调用,就好像它是MyData实例成员!图 7-22 说明了代码。类MyDataExtendMyData一起像期望的类一样工作,有三个方法。

`   namespace ExtensionMethods    {       sealed class MyData       {          private double D1, D2, D3;          public MyData(double d1, double d2, double d3)          { D1 = d1; D2 = d2; D3 = d3; }

         public double Sum() { return D1 + D2 + D3; }       }

      static class ExtendMyData       Keyword and type       {                                    ↓               public static double Average(this MyData md)          {        ↑             Declared static             return md.Sum() / 3;          }       }

      class Program       {          static void Main()          {             MyData md = new MyData(3, 4, 5);             Console.WriteLine("Sum:     {0}", md.Sum());             Console.WriteLine("Average: {0}", md.Average());          }                                          ↑       }                                 Invoke as an instance member of the class    }`

该代码产生以下输出:


Sum:     12 Average: 4


命名约定

写程序需要想出很多名字;类、变量、方法、属性的名字,还有很多我还没有提到的东西。当你通读一个程序时,使用命名约定是一个重要的方法,可以给你一个关于你正在处理的对象种类的线索。

我在第六章中提到了一点命名,但是现在你已经知道了更多关于类的知识,我可以给你更多的细节。表 7-4 给出了三种主要的命名方式以及它们在中的常用方式 .NET 程序。

Image

不是每个人都同意这些约定,尤其是前导下划线部分。我自己发现前导下划线非常难看,但很有用,并在我自己的代码中将其用于私有和受保护的变量。微软自己在这个问题上似乎也有矛盾。在其建议的约定中,Microsoft 不将前导下划线作为选项。但是他们在自己的代码中使用了它。

在本书的其余部分,我将坚持微软官方推荐的对私有和受保护字段使用 camel 套管的惯例。

关于下划线的最后一点是,它们通常不在标识符的主体中使用,除了在事件处理程序的名字中,我将在第十四章中介绍。