3.5 结构
前面介绍了类如何封装程序中的对象,也介绍了如何将它们存储在堆中,通过这种方式可以在数据的生存期上获得很大的灵活性,但性能会有一定的损失。因为托管堆的优化,这种性能损失比较小。但是,有时仅需要一个小的数据结构。此时,类提供的功能多于我们需要的功能,由于性能原因,最好使用结构。看看下面的例子:
public class Dimensions { public double Length { get; set; } public double Width { get; set; } }
上面的代码定义了类Dimensions,它只存储了某一项的长度和宽度。假定编写一个布置家具的程序,让人们试着在计算机上重新布置家具,并存储每件家具的尺寸。表面看来使字段变为公共字段会违背编程规则,但这里的关键是我们实际上并不需要类的全部功能。现在只有两个数字,把它们当成一对来处理,要比单个处理方便一些。既不需要很多方法,也不需要从类中继承,也不希望.NET运行库在堆中遇到麻烦和性能问题,只需要存储两个double类型的数据即可。
为此,只需要修改代码,用关键字struct代替class,定义一个结构而不是类,如本章前面所述:
public struct Dimensions { public double Length { get; set; } public double Width { get; set; } }
为结构定义函数与为类定义函数完全相同。下面的代码说明了结构的构造函数和属性(代码文件StructsSample/Dimension.cs):
public struct Dimensions { public double Length { get; set; } public double Width { get; set; } public Dimensions(double length, double width) { Length = length; Width = width; } public double Diagonal => Math.Sqrt(Length * Length + Width * Width); }
结构是值类型,不是引用类型。它们存储在栈中或存储为内联(如果它们是存储在堆中的另一个对象的一部分),其生存期的限制与简单的数据类型一样。
● 结构不支持继承。
● 对于结构,构造函数的工作方式有一些区别。如果没有提供默认的构造函数,编译器会自动提供一个,把成员初始化为其默认值。
● 使用结构,可以指定字段如何在内存中布局(第16章在介绍特性时将详细论述这个问题)。
因为结构实际上是把数据项组合在一起,所以有时大多数或者全部字段都声明为public。严格来说,这与编写.NET代码的规则相反——根据Microsoft,字段(除了const字段之外)应总是私有的,并由公有属性封装。但是,对于简单的结构,许多开发人员都认为公有字段是可接受的编程方式。
下面几节将详细说明类和结构之间的区别。
3.5.1 结构是值类型
虽然结构是值类型,但在语法上常常可以把它们当作类来处理。例如,在上面的Dimensions类的定义中,可以编写下面的代码:
var point = new Dimensions(); point.Length = 3; point.Width = 6;
注意,因为结构是值类型,所以new运算符与类和其他引用类型的工作方式不同。new运算符并不分配堆中的内存,而是只调用相应的构造函数,根据传送给它的参数,初始化所有的字段。对于结构,可以编写下述完全合法的代码:
Dimensions point; point.Length = 3; point.Width = 6;
如果Dimensions是一个类,就会产生一个编译错误,因为point包含一个未初始化的引用——不指向任何地方的一个地址,所以不能给其字段设置值。但对于结构,变量声明实际上是为整个结构在栈中分配空间,所以就可以为它赋值了。但要注意下面的代码会产生一个编译错误,编译器会抱怨用户使用了未初始化的变量:
Dimensions point; double D = point.Length;
结构遵循其他数据类型都遵循的规则:在使用前所有的元素都必须进行初始化。在结构上调用new运算符,或者给所有的字段分别赋值,结构就完全初始化了。当然,如果结构定义为类的成员字段,在初始化包含的对象时,该结构会自动初始化为0。
结构会影响性能的值类型,但根据使用结构的方式,这种影响可能是正面的,也可能是负面的。正面的影响是为结构分配内存时,速度非常快,因为它们将内联或者保存在栈中。在结构超出了作用域被删除时,速度也很快,不需要等待垃圾回收。负面影响是,只要把结构作为参数来传递或者把一个结构赋予另一个结构(如A=B,其中A和B是结构),结构的所有内容就被复制,而对于类,则只复制引用。这样就会有性能损失,根据结构的大小,性能损失也不同。注意,结构主要用于小的数据结构。
但当把结构作为参数传递给方法时,应把它作为ref参数传递,以避免性能损失——此时只传递了结构在内存中的地址,这样传递速度就与在类中的传递速度一样快了。但如果这样做,就必须注意被调用的方法可以改变结构的值。
3.5.2 结构和继承
结构不是为继承设计的。这意味着:它不能从一个结构中继承。唯一的例外是对应的结构(和C#中的其他类型一样)最终派生于类System.Object。因此,结构也可以访问System.Object的方法。在结构中,甚至可以重写System.Object中的方法——如重写ToString()方法。结构的继承链是:每个结构派生自System.ValueType类,System.ValueType类又派生自System.Object。ValueType并没有给Object添加任何新成员,但提供了一些更适合结构的实现方式。注意,不能为结构提供其他基类:每个结构都派生自ValueType。
3.5.3 结构的构造函数
为结构定义构造函数的方式与为类定义构造函数的方式相同。
前面说过,默认构造函数把数值字段都初始化为0,且总是隐式地给出,即使提供了其他带参数的构造函数,也是如此。
在C# 6中,也可以实现默认的构造函数,为字段提供初始值(这一点在早期的C#版本中未实现)。为此,只需要初始化每个数据成员:
public Dimensions() { Length = 0; Width = 1; } public Dimensions(double length, double width) { Length = length; Width = width; }
另外,可以像类那样为结构提供Close()或Dispose()方法。第5章将讨论Dispose()方法。