第 42 讲:结构(一):值类型的定义和结构的自定义
各位已经接触到了一些值类型和引用类型的基本概念。为了衔接本版块的内容,我们还要重新介绍一次。不过因为要接触具体内容了,因此我们要说得更细一点。
Part 1 值类型和引用类型
C# 把所有的数据类型全部按照值类型(Value Type)和引用类型(Reference Type)分为两类。值类型可根据数据类型的安放和存储对应进入堆内存或栈内存里;而引用类型只能存储在堆内存里。
之前有简单说过,堆内存相对于运算过程来说要远一些,所以存储和取出数值可能会稍微慢一点;而栈内存更快。因此,如果我们需要优化计算速度的话,应该优先考虑值类型(因为值类型可以存储在栈内存里,而引用类型不能)。
另外,值类型和引用类型的存储规则是无法修改的,你只能通过代码的方式来使用它们;如果你需要改变存储规则,唯一的办法就是把变量的类型从值类型改成引用类型;或者从引用类型改成值类型。
另一方面,值类型是可存储在栈内存里的,因此存储在栈内存里的这一部分值类型数据是不受到垃圾回收器 GC 的管理的。GC 在之前已经说过了它的基本执行模式:找到不再使用的堆内存数据空间,然后销毁掉后,通过紧凑处理把数据压缩放在一起避免零碎的空间。但是,因为它只管辖堆内存,而栈内存是通过方法自身的调用和释放而自动产生内存空间和销毁空间的,因此和 GC 无关。要知道,GC 怎么着都会处理比栈内存存储更多的数据,那么速度显然就会比栈内存慢,因此值类型更好的一点是优化存储机制。
所以,我们这里知道了两个值类型比引用类型更好的地方:
值类型存储在栈内存的这一块,自动受到方法本身管理,不受 GC 管理;
值类型计算速度更快。
之前我们讲的接口、类都是引用类型,因为它们往往都较大,所以放在堆内存里是正合适的一种手段。但是,我们需要用到栈内存的时候,却因为语法不能支持,导致很头疼的境况。下面我们来说一下,一个值类型应该如何自定义。
Part 2 结构的定义
下面,我们引入一种和类、接口的地位同等重要的另外一种自定义的数据类型:结构(Structure)。结构和类的定义方式基本完全一致,唯一的区别是,把类的 class
关键字改写成 struct
。
我们唯一改变的地方就是 class
改成了 struct
,别的地方一点变化都没有。这个 Person
此时被称为一个结构(即 Person
结构)。它和一般的类基本用法都差不多,但是有一些细节可能和类不一样。下面我们简单说一下。
Part 3 结构在使用上和类不一样的地方
刚才说到,结构和类的定义差别仅仅是在 struct
改成了 class
,那么它们细节上又有什么不同的地方呢?
在类里,如果不定义无参构造器的话,系统会自动生成一个,而且里面啥操作都没有的无参构造器。在结构里,无参构造器是永远都存在的,如果你不定义的话,它会存在;而另一方面,即使你自己手写,编译器也会报错,告知你无法自己定义无参构造器,因为无参构造器是系统赋予的一种特殊机制,你无权更改,只能使用。
换句话说,上面这一段文字就说的是这个情况:
Person
此时是结构的话,那么这么书写代码必然就会出错。Person
的无参构造器是系统保留下来的固定存在的机制。因此请和类在这一点上进行区分。
那么,为什么会这样呢?值类型为啥不让自定义无参构造器呢?我们之前有说过一个东西,所有的系统类型(除了 string
和 object
),都是值类型。这些值类型都有各自的字面量书写格式。比如 decimal
用后缀 M
标记;int
则直接一个整数就可以了,等等这样的东西。实际上在系统执行期间,系统会为这些值类型单独分配内存空间提供变量的初始化和使用。但是,一旦我们可以自定义无参构造器的话,我们就相当于更改了这些值类型的初始化行为。系统只要默认生成一个值类型,那么必然就得使用无参构造器对变量的内存空间执行操作有一个简单的规划,而无参构造器虽然里面没有代码,但它也必不可少。
所以,值类型的无参构造器是我们不可改变的、固有的一种机制;这一点和类不一样。接口就更不用说了,接口压根不让声明构造器,因为接口是用来提供给别的类型的一种约束的,自身是无法实例化的。
3-2 结构里的数据成员必须都在构造器里给出初始化
除了结构的无参构造器的声明行为不一样以外,结构的构造器里,必须给出所有数据成员的初始化。这里我们要把数据成员提出来给大家说一下概念。之前也是简单用了一下这些词语,但因为没有体系化说明,所以这里说明一下。
数据成员(Data Member),指的是类和结构里的这些实例字段。之所以称为数据成员,是因为它们专门用来存储数据,而字段本身就是类或者结构类型里的一种成员类别,因此称为数据成员。方法、索引器等等别的成员都不属于数据成员,因为它们多数体现出来都是跟方法执行的行为差不多:即在处理一些数据,而不是单纯的存储数据。
那么,一旦我们定义出了一个结构,那么里面的这些字段就必须赋值。在类里,我们即使不给字段赋值,字段也会默认得到一个分配的数据结果;但在结构里,所有的字段都必须得到赋值,否则编译器就会告诉你这么写是错的。
_age
赋值,编译器就会告诉你,_age
我们只需要追加一个 : this()
的调用,就可以了。
3-3 数据成员无法使用等号直接赋值
在类里,我们可以直接在字段的末尾追加 = 数据
的语法来给字段赋值。但是在结构里,这一点是不允许的。
如上代码所示,这种语法只可能在类里出现,结构是无法直接对字段赋值的。至于原因……因为结构的初始化是需要通过构造器这种严格处理机制对每个成员给出赋值才可保证结构使用和实例化的安全性,而这种书写格式因为是跳过了构造器对字段的初始化,所以会导致初始化的不安全(用户看起来很困惑,以及编译器对这个分析的复杂度会增加)。
总的来说,就是有点别扭,因此 C# 干脆不让你这么写了。