【原创】《.net本质论》读书笔记三
上一章讲的是Type,这一章讲了实例在内存中的情况。主要内容有两个——分清楚值类型和引用类型;搞清楚GC是怎么回事。这其中当然也介绍了一些有用的函数了。
1、值类型和引用类型
值类型是继承于System.ValueType的类型,它与引用类型一样可以有字段、方法,但是不能被继承,运行时是存储在栈上的。
值类型在CLR中只有三种:基本数据类型,如Int32,Boolean;struct;enum。
引用类型则是继承于System.Object的,其运行时的内存结构中有对象头,记录虚函数等信息,并是存储在托管堆上的。
当编译器遇到c#中的int、bool等原始类型时,会将它们翻译成CLR的Int32、Boolean类型,在运行时,这些Int32、Boolean不像引用类型的实例,有对象头。虽然System.ValueType也是继承于System.Object,但是ValueType这个类就告诉了编译器,要将这些类型区别对待。
代码中,Oil和price都是值类型,看中间代码:
可以看出,Oil是继承于ValueType的,(值得注意的是:c#代码中不可以写:ValueType来表示继承,当写struct时,就是隐式继承于ValueType,而且编译器会隐式地加上sealed关键字),price也被翻译成Int32了。
enum(枚举)也是一个值类型, enum默认是Int32类型,而且如果没有指定显式值的话,默认从0,1,2这样开始。enum的类型也可以指定类型,如下:
可以用enum实现bitmask(位掩码),如下:
用[System.Flags]属性时,enum底层的ToString()方法也会改变。如下:
CarOptions里的值分别为1、2、4、8,从二进制角度看就是一个位值,所以叫位掩码。上面这个代码输出了ToString()是:
这时,(int)car的值是7.
总结值类型和引用类型的不同:
a、分配内存地方不同,值类型在stack上,引用类型所指对象在managed heap上。
b、值类型是继承于ValueType的,后者不是。
c、二者都可以有构造函数并通过new来调用。不同在,前者的new被编译器翻译为iniobj,后者被翻译为newobj,而且后者声明有参ctor后不可以有默认ctor,前者可以。
d、前者只能实现接口,不能继承类,后者可以实现接口、继承类。
函数传参时是值传递,那如果struct里有一个引用类型呢?这时传入的还是struct的值,不过当改变struct里引用类型对象的field时,这时做改变的是托管堆上的内容,所以,传入值看似也变了,其实还是值传递。
2、相等(equivalent)和同一(identical)
对于值类型,判断相等和同一是一样的,只是指的比较。引用类型则不同,equivalent是说对象内部值相等,identical是说引用是否指向同一个对象。判断两个引用是否指向同一个对象,用Object.ReferenceEquals(Object o), 也可以用==(在c++或c#里)。但是,由于运算符可以重载,所以不建议用==判断。如果使用Object.GetHashCode()的返回值来判断,两个对象的hashcode不相等,则肯定不是一个引用;如果相等,也不一定是一个对象。因为不同对象通过hash函数也可以得出相同hash值。
对于equivalent,需要自定义类型重载Object的Equals(Object o),才能正确判断对象是否相等。
一般来说,Object.GetHashCode(), Object.Equals(), IComparable.CompareTo()是需要同时override的。
3、拷贝
从一个对象拷贝出另一个对象,用Object.MemberwiseClone(), 这个是shallow copy(即值拷贝)。要实现对象的deep copy,需要实现ICloneable接口,覆写Clone()函数。String类的Clone也是shallow copy,用String.Copy(string str)可以新创建实例。
4、boxing、unBoxing
装箱操作发生在把一个值类型赋给一个引用,拆箱操作发生在把一个对象转换为值类型。注意:int到Int32是编译器映射的,不算是装箱。
5、多维数组
CLR支持两种多维数组——一种是c类型的数组,每行的元素个数相等;一种是交错(jagged)数组,每行的元素个数可以不等。以二维为例:
第一种的声明方式: int[,] array = new [3,4]; // 三行两列
第二种的声明方式: int[][] array = new [3][]; //三行,每行个数不定
第一种可以通过array.GetLength(int dimension)获取每一维的长度,start with 0;
第二种需要array[0] = new int[8]来定义每一行。
6、关于GC
CLR的GC通过查找所有引用,以此识别哪些对象是不可以回收的,其余的托管内存就可以回收。但实际上没有那么简单。
a、根引用vs非根引用
根引用是static引用或者local variable,非根引用是对象的字段。能通过根引用直接或间接找到的是reachable的对象,其他是unreachable的。词法上的存活期不代表这个对象就是reachable的,CLR通过JIT编译器的liveness信息来判断一个local variable是否是存活的。
b、当确定了哪些对象是unreachable的,GC就可以回收这些内存。至于GC何时、如何回收,程序员无需关心。
c、终结处理(Object Finalization)
如果想在对象被回收时写清理代码,可以重写Finalize()函数。不过这个函数在c#中不可以直接写,而是用析构函数来表示的。编译器会在Finalize方法里用析构函数的代码,最后调用基类型的Finalize方法。
这个过程是异步的,什么意思呢?当GC开始回收时,发现这个对象虽然是unreachable但是有Finalize方法,这时GC不会立即回收这个对象,而是将它挂到FinalizeQueue上(就是这个queue有一个referece指向对象)等待GC的终结线程来执行这个方法。这样这个对象会survive到下一个generation。这导致回收线程和终结线程不同时发生,甚至相隔时间较大,异步产生。
使用Finalize方法会让对象存活时间变长,而且会占用两次回收过程,不建议使用,在实际编程中,应该用Dispose方法。
d、Dispose()
实现了IDispose()接口,通过程序员主动调用Dispose()进行资源清理,并在Dispose()函数里写System.GC.SuppressFinalize(this)的方法,不让对象挂到FinalizeQueue上,这样,对象是即时被回收的,而且没有进入下一个generation,同时也节省了一个回收过程。
利用using语法,在对象离开using范围时,Dispose自动被调用,即时清理资源。如下:
e、弱引用(weakReference)
我们上面说的都是强引用,就是说有引用直接指向对象。GC不会回收强引用指向的对象。那么,当一个对象我们需要访问,但是如果GC想回收也可以回收的时候,就要用到弱引用System.WeakReference。举例说 :
上面代码详细介绍了弱引用的使用。但是有一个问题,当GC回收后,我run代码输出
debug时输出:
这是为什么呢?