C#编程语言详解(第2版) 11.3 类和结构的区别
11.3 类和结构的区别
结构在以下几个重要方面与类不同:
— 结构是值类型(参见11.3.1小节)。
— 所有结构类型都隐式地继承自类System.ValueType(参见11.3.2小节)。
— 对结构类型的变量进行赋值,将创建所赋的值的一个“副本”(参见11.3.3小节)。
— 结构的默认值的生成方式为:将所有值类型的域设置为它们的默认值,并将所有引用类型的域设置为null(参见11.3.4小节)。
— 使用装箱和拆箱操作在结构类型和object之间进行转换(参见11.3.5小节)。
— 对于结构,this具有不同的意义(参见11.3.6小节)。
— 结构的实例域声明不能包含变量初始值设定项(参见11.3.7小节)。
— 结构不能声明无参数的实例构造函数(参见11.3.8小节)。
— 结构不能声明析构函数(参见11.3.9小节)。
11.3.1 值语义
结构是值类型(参见4.1节),并且被称为具有值语义。另一方面,类是引用类型(参见4.2节),并且被称为具有引用语义。
结构类型的变量直接包含了结构的数据,而类类型的变量只包含了对数据的引用(称为“对象”)。如果结构B包含一个类型为A的实例域,并且A为结构类型,则如果A依赖于B,将发生编译时错误。如果结构X包含一个类型为Y的实例域,则结构X直接依赖于结构Y。从这个定义可以推断出,一个结构所依赖的结构的完整集合就是此直接依赖于关系的传递闭包。例如:
struct Node
{
int data;
Node next; //错误,Node直接依赖于其自身
}
是错误的,因为Node包含了一个其自身类型的实例域。另一个示例:
struct A { B b; }
struct B { C c; }
struct C { A a; }
也是错误的,因为类型A、B和C彼此相互依赖。
对于类,两个变量可能会引用同一个对象,因此对一个变量进行的操作可能会影响另一个变量所引用的对象。对于结构,每个变量都具有其自己的数据副本(ref和out参数变量除外),因此对一个变量的操作不会影响其他变量。另外,由于结构不是引用类型,因此结构类型的值不可能为null。
给定以下声明:
struct Point
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
以下代码片段将输出值10:
Point a = new Point(10, 10);
Point b = a;
a.x = 100;
System.Console.WriteLine(b.x);
将a赋给b将创建该值的一个副本,因此b不会受到对a.x进行的赋值的影响。如果将Point声明为类,则由于a和b将引用同一个对象,因此输出将为100。
11.3.2 继承
所有的结构类型都隐式地继承自类System.ValueType,而后者则继承自object类。结构声明可以指定一个实现接口的列表,但是不能指定基类。
结构类型永远不会是抽象的,并且始终是隐式密封的。因此在结构声明中不允许使用abstract和sealed修饰符。
由于结构不支持继承,所以结构成员的声明可访问性不能是protected或protected internal。
结构中的函数成员不能是abstract或virtual,因而override修饰符只允许用于重写继承自System.ValueType的方法。
11.3.3 赋值
对结构类型变量的赋值将创建一个所赋值的“副本”。这不同于对类类型变量的赋值,后者复制的是引用,而不是由引用所标识的对象。
与赋值类似,将结构作为值参数传递或者作为函数成员的结果返回时,将创建该结构的一个副本。结构可通过ref或out参数以引用方式传递给函数成员。
当结构的属性或索引器是赋值的目标时,与属性或索引器访问相关联的实例表达式必须归类为一个变量。如果该实例表达式归类为一个值,则将发生编译时错误。7.13.1小节对此进行了详细的说明。
11.3.4 默认值
如5.2节所述,有几种变量在创建时将自动初始化为它们的默认值。对于类类型和其他引用类型的变量,此默认值为null。但是,由于结构是不能为null的值类型,因此结构的默认值,是通过将所有值类型域设置为它们的默认值,并将所有引用类型域设置为null而产生的值。
对于上面示例中所声明的Point结构,示例:
Point[] a=new Point[100];
会将数组中的每个Point初始化为通过将x和y域设置为零而产生的值。
一个结构的默认值对应于该结构的默认构造函数所返回的值(参见4.1.1小节)。与类不同,结构不允许声明无参数的实例构造函数。相反,每个结构都隐式地具有一个无参数的实例构造函数,该构造函数总是返回通过将所有值类型的域设置为它们的默认值,并将所有引用类型的域设置为null而得到的值。
在设计结构时,要设法确保其默认初始化状态是有效的状态。在下面的示例中:
using System;
struct KeyValuePair
{
string key;
string value;
public KeyValuePair(string key, string value) {
if (key == null || value == null) throw new ArgumentException();
this.key = key;
this.value = value;
}
}
除非在显式调用时,否则用户自定义的实例构造函数不允许出现null值。在变量KeyValuePair可能会被初始化为它的默认值的情况下,key和value域都将为null,所以设计该结构时,必须正确处理好此问题。
11.3.5 装箱和拆箱
类类型的值可以转换为object类型或由该类实现的接口类型,这只需在编译时将对应的引用当作另一个类型处理即可。与此类似,object类型的值或接口类型的值也可以转换回类类型,而不必更改相应的引用。当然,在这种情况下,需要进行运行时类型检查。
由于结构不是引用类型,因此上述操作对结构类型是以不同的方式实现的。当结构类型的值转换为object类型或一个由该结构实现的接口类型时,将会执行一次装箱操作。与此类似,当object类型的值或接口类型的值转换回结构类型时,将会执行一次拆箱操作。与对类类型进行的相同操作相比,主要区别在于:装箱操作会把相关的结构值复制为一个已装箱的实例,而拆箱则会从已装箱的实例中复制出一个结构值。因此,在进行装箱或拆箱操作之后,对未装箱的结构所进行的更改不会影响已装箱的结构。
有关装箱和拆箱的详细信息,请参见4.3节。
11.3.6 this的意义
在类的实例构造函数或实例函数成员中,this归类为一个值。因此,虽然this可以用于引用为其调用函数成员的实例,但是不能在类的函数成员中对this进行赋值。
在结构的实例构造函数内,this对应于一个结构类型的out参数,而在结构的实例函数成员内,this对应于一个结构类型的ref参数。在这两种情况下,this都将归类为一个变量,因此可以通过对this进行赋值,或通过将this作为ref或out参数进行传递,而对为其调用函数成员的整个结构进行修改。
11.3.7 域初始值设定项
如11.3.4小节所述,结构的默认值由将所有值类型的域设置为它们的默认值并将所有引用类型的域设置为null而产生的值组成。由于这个原因,结构不允许它的实例域声明包含变量初始值设定项。此限制只适用于实例域。结构的静态域声明可以包含变量初始值设定项。
示例:
struct Point
{
public int x = 1; //错误,不允许包含初始值设定项
public int y = 1; //错误,不允许包含初始值设定项
}
将出现错误,因为实例域声明中包含了变量初始值设定项。
11.3.8 构造函数
与类不同,结构不允许声明无参数的实例构造函数。相反,每个结构都隐式地包含一个无参数的实例构造函数,该构造函数总是返回通过将所有值类型的域设置为它们的默认值,并将所有引用类型的域设置为null而得到的值(参见4.1.2小节)。结构可以声明具有参数的实例构造函数。例如:
struct Point
{
int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
给定以上声明,语句:
Point p1 = new Point();
Point p2 = new Point(0, 0);
都将创建一个x和y都初始化为零的Point。
结构的实例构造函数不能包含形式为“base(…)”的构造函数初始值设定项。
如果结构的实例构造函数没有指定构造函数初始值设定项,则this变量将相当于一个结构类型的out参数,并且与out参数类似,this必须在该结构函数返回的每个位置都已明确赋值(参见5.3节)。如果结构的实例构造函数指定了一个构造函数初始值设定项,则this变量将相当于一个结构类型的ref参数,并且与ref参数类似,this将被视为在进入构造函数主体时已明确赋值。在下面的实例构造函数实现中:
struct Point
{
int x, y;
public int X {
set { x = value; }
}
public int Y {
set { y = value; }
}
public Point(int x, int y) {
X = x; //错误,this未明确赋值
Y = y; //错误,this未明确赋值
}
}
在被构造的结构的所有域都已明确赋值之前,不能调用任何实例成员函数(包括属性X和Y的set访问器)。但是请注意,如果Point是类而不是结构,则允许上述的实例构造函数。
11.3.9 析构函数
结构不允许声明析构函数。
11.3.10 静态构造函数
结构的静态构造函数与类的静态构造函数所遵循的规则大体相同。在应用程序域中第一次发生以下事件时,将触发结构的静态构造函数的执行:
— 结构的实例成员被引用。
— 结构的静态成员被引用。
— 结构的显式声明的构造函数被调用。
创建结构类型的默认值(参见11.3.4小节)不会触发静态构造函数(例如数组中元素的初始值)。