深入理解类

思考并回答以下问题:

  • 类的成员都有哪些?
  • 一个类的多个实例的成员数据在内存中是怎么存储的?
  • 静态成员的数据是怎么存储的?

本章涵盖:

  • 类成员
  • 成员修饰符的顺序
  • 实例类成员
  • 静态字段
  • 静态函数成员
  • 其他静态类成员类型
  • 成员常量
  • 常量和静态
  • 属性
  • 实例构造函数
  • 静态构造函数
  • 对象初始化语句
  • 析构函数
  • readonly修饰符
  • this关键字
  • 索引
  • 访问器的访问修饰符
  • 分部类和分部类型
  • 分部方法

类成员

之前的两章阐述了9种类成员类型中的两种:字段和方法。在这一章中,我会介绍除事件和运算符之外的类型的类成员,并讨论其特征。

表1列出了类的成员类型。已经介绍过的类型用菱形标记。将在本章阐述的类型用勾号标记。将在以后的章节中阐述的类型用空的选择框标记。

表1 类成员的类型

成员修饰符的顺序

在前面的内容中,你看到字段和方法的声明可以包括如public和private这样的修饰符。这一章会讨论许多其他的修饰符。多个修饰符可以在一起使用,自然就产生一个问题:它们需要按什么顺序排列呢?

类成员声明语句由下列部分组成:核心声明、一组可选的修饰符和一组可选的特性(attribute),用于描述这个结构的语法如下。方括号表示方括号内的成分是可选的。

1
[特性] [修饰符] 核心声明

  • 修饰符
    • 如果有修饰符,必须放在核心声明之前。
    • 如果有多个修饰符,可以是任意顺序。
  • 特性
    • 如果有特性,必须放在修饰符和核心声明之前。
    • 如果有多个特性,可以是任意顺序。

例如,public和static都是修饰符,可以用在一起修饰某个声明。因为它们都是修饰符,所以可以放置成任何顺序。下面两行代码是语义等价的:

1
2
public static int MaxVal;
static public int MaxVal;

图1阐明了声明中各成分的顺序,到目前为止,它们可用于两种成员类型:字段和方法。注意,字段的类型和方法的返回类型不是修饰符——它们是核心声明的一部分。

图1 类成员的类型

实例类成员

类成员可以关联到类的一个实例,也可以关联到类的整体,即所有类的实例。默认情况下,成员被关联到一个实例。可以认为是类的每个实例拥有自己的各个类成员的副本,这些成员称为实例成员

改变一个实例字段的值不会影响任何其他实例中成员的值。迄今为止,你所看到的字段和方法都是实例字段和实例方法。

例如,下面的代码声明了一个类D,它带有唯一整型字段Mem1, Main创建了该类的两个实例,每个实例都有自己的字段Mem1的副本,改变一个实例的字段副本的值不影响其他实例的副本的值。图6-2阐明了类D的两个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class D
{
public int Mem1;
}

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

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

这段代码产生如下输出:

1
d1 = 10, d2 = 28

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

静态字段

除了实例字段,类还可以拥有静态字段。

  • 静态字段被类的所有实例共享,所有实例都访问同一内存位置。因此,如果该内存位置的值被一个实例改变了,这种改变对所有的实例都可见。
  • 可以使用static修饰符将字段声明为静态,如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class D
    {
    int Mem1; // 实例字段
    static int Mem2; // 静态字段
    // ↑ 关键字
    }

    class Program
    {
    static void Main()
    {
    D d1 = new D();
    D d2 = new D();
    ...
    }
    }

例如,图3左边的代码声明了类D,它含有静态字段Mem2和实例字段Mem1,Main定义了类D的两个实例。该图表明静态成员Mem2是与所有实例的存储分开保存的。实例中灰色的字段表明,从实例内部,访问或更新静态字段的语法和访问或更新其他成员字段一样。

  • 因为Mem2是静态的,类D的两个实例共享单一的Mem2字段。如果Mem2被改变了,这个改变在两个实例中都能看到。
  • 成员Mem1没有声明为static,所以每个实例都有自己的副本。

图3 静态和非静态数据成员

从类的外部访问静态成员

在前一章中,我们看到使用点运算符可以从类的外部访问public实例成员。点运算符由实例名、点和成员名组成。

就像实例成员,静态成员也可以使用点运算符从类的外部访问。但因为没有实例,所以必须使用类名,如下面代码所示:

1
D.Mem2 = 5; // 访问静态成员

静态成员的生存期

静态成员的生命期与实例成员的不同。

  • 之前我们已经看到了,只有在实例创建之后才产生实例成员,在实例销毁之后实例成员也就不存在了。
  • 但是即使类没有实例,也存在静态成员,并且可以访问。

图4阐述了类D,它带有一个静态字段Mem2。虽然Main没有定义类的任何实例,但它把值5赋给该静态字段并毫无问题地把它打印出来。

图4 没有类实例的静态成员仍然可以被赋值并读取,因为字段与类有关,而与实例无关

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

1
Mem2 = 5

说明
静态成员即使没有类的实例也存在。如果静态字段有初始化语句,那么会在使用该类的任何静态成员之前初始化该字段,但没必要在程序执行的开始就初始化。

静态函数成员

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

  • 如同静态字段,静态函数成员独立于任何类实例。即使没有类的实例,仍然可以调用静态方法。
  • 静态函数成员不能访问实例成员。然而,它们能访问其他静态成员。

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

0%