【C#进阶系列】05 基元类型、引用类型和值类型

C#

浏览数:132

2019-6-26

AD:资源代下载服务

博客园韩子卢【C#进阶系列】文章目录

 基元类型和FCL类型

FCL类型就是指Int32这种类型,这是CLR支持的类型。

而基元类型就是指int这种类型,这是C#编译器支持的,实际上在编译后,还是会被转为Int32类型。

而且学过C的朋友肯定记得,int在32位机器和64位机器字节数可能不同,但是C#.NET里int就是表示Int32。

因为在基元类型和FCL类型之间,有一个一一对应的映射关系。另外注意dynamic实际上对应的类型就是Object,只是说C#编译器允许用简单的语法让dynamic变量参与动态调度。

表达式由字面量构成,编译器在编译的时候就能完成表达式求值

Boolean found=false;//生成的代码将found设为0
Int32 x=100+20+3;//x设为123
String a="a"+"bc";//s设为“abc”

checked和unchecked基元类型操作

此指令就用来检查溢出和不检查溢出,而默认是unchecked,不过这个可以改。检查溢出就报异常,不检查溢出就回滚。

引用类型和值类型

所谓值类型的去看它们类型的定义,比如Int32和DayOfWeek都是struct和enum类型,而struct类型实际上派生自System.ValueType类型,而Enum类型派生自System.Enum类型。而Enum类型最终还是派生自System.ValueType类型。

好吧,他们俩个的差异性其实还是蛮多的,不过基本上这是最基础的了,而且基本上是本书就讲,所以反而懒得写了。

装箱与拆箱

装箱就是把本来在栈中的值类型,在堆中新开辟一个内存空间,把值类型的数据复制进去,并增加引用类型都有的类型指针和同步块索引,然后返回这个内存空间引用地址。

拆箱就是反过来,先获取装箱对象中各个字段的地址,再将这些字段包含的值从堆复制到栈。

由上面看出装箱拆箱其实很影响效率,所以写代码的时候应该避免。

另外装箱的值类型,调用自身的函数修改自己的字段的时候并不会修改堆里的数据,只会先转换为值类型,再修改栈里的数据。

如果要修改堆里的数据,只能定义一个接口,让值类型里的函数去实现这个接口,然后想修改堆里的数据,那么就把对象转换为这个接口,那么去修改的话,就会修改堆里的数据。

如果你看不懂上面的话,那么请慎用值类型,作者推荐不要定义任何会修改实例字段的属性或者方法,甚至可以将值类型的所有字段都加上readonly。

其实结构体什么的用类就好了,大的结构体传参啊什么的,搞不好还会引起栈溢出。毕竟每个线程也就1MB的栈空间。

对象的相等性和同一性

之前我们讲过System.Object提供了名为Equals的虚方法,也就是说所有的对象都是有的,作用实在两个对象包含相同值的前提下返回true。

然而这个方法只是比较了同一性,而不是相等性。

实际上就是对应的同一性就是指两个对象的引用相同,也就是说它们指向同一个对象。

相等性如字面意思可知。也就说如果具备同一性,那么一定具备相等性。

由于Equals是个虚方法,可以重写,所以并不一定就是这个用法。(System.ValueType就重写了,Equals实现的是相等性而不是统一性。但是这个Equals是里的实现步骤用到了反射,而反射这个东西又是比较慢的,所以定义自己的值类型时可以考虑重写,从而提高性能。)

而==这个操作符也是可以重载的,除非你在比较之前,将两个对象的类型都转换为object。

于是Object又提供了一个静态方法,Object.ReferenceEquals,效果就如上所述,比较同一性。

注意,如果自己去定义一个值类型,然后重写Equals方法去实现相等性。那么应该注意让类型实现IEquatable<T>接口的Equals方法(通常实现的Equals方法除了调用自己的类型参数,还应该有一个重载函数调用object参数,以便在内部调用类型安全的Equals方法。这个定义接收object对象的重写函数么就是对IEquatable<T>的Equals的实现),还有重写==和!=操作符方法。

考虑到排序,所以可能还需要实现IComparable的CompareTo方法,和IComparable<T>的类型安全的CompareTo方法。实现了这些方法,那么<,<=,>,>=在内部调用类型安全的CompareTo方法也OK了。

(如果你觉得上面自己定义值类型的实现还有什么地方觉得遗漏,最好的方法其实就是去看int类型的定义就ok了)

对象哈希码

System.Object提供了虚方法GetHashCode,它能获取任意对象的Int32哈希码。

另外重写了Equals方法,那么最好重写GetHashCode方法。

因为在System.Collections.Hashtable类型和System.Collections.Generic.Dictionary类型以及其它的一些集合的实现中,要求两个对象必须要有相同的Hash码才被视为相等。

所以重写Equals方法实现相等性后,最好也重写GetHashCode方法,以确保相等型算法和对象哈希码算法一致。

另外重写时可以调用基类的GetHashCode方法,但是不要调用Object或者ValueType的GetHashCode方法,因为两者的实现性能不高。

包含相同值的两个不同对象应返回相同的哈希码。(作者建议不要对哈希码进行持久化,因为哈希码的算法可能会改变)

dynamic基元类型

dynamic基元类型是为了方便开发人员使用反射或者与其它非.net组件通信.

代码使用dynamic表达式/变量时,编译器生成特殊的IL代码来描述这种操作。这种特殊的代码被称为payload(有效载荷)。在运行时,payload根据dynamic表达式/变量引用的对象的实际类型来决定具体执行的操作。

dynamic类型在编译后实际上是作为System.object,然而它在元数据中被应用了System.Runtime.CompilerServices.DynamicAttribute的实例。局部变量除外,因为Attribute显然不能在方法内部使用。

另外使用的泛型的dynamic的代码时,泛型代码已经变异好了,将类型视为Object,编译器不在泛型代码中生成payload,所以也不会执行动态调度。

且编译器允许使用隐式转型语法,将表达式从dynamic转型为其它类型。

dynamic a=123;
Int32 b=a;

另外dynamic表达式的求值结果也是一个dynamic类型。

不能定义对dynamic进行扩展的扩展方法,不能将lambda表达式或匿名方法作为实参传给dynamic使用。

为COM对象生成可由“运行时”调用的包装时。Com组件的方法中使用任何Variant实际都转化为dynamic,这称为动态化。显著简化了与COM对象的操作。

当然用dynamic会有额外的性能开销,因为会引用一些必须的dll,然后执行一些动态绑定啊什么的。如果只是一两处用这个东西,还是用传统方法好一点。(一般会引用Microsoft.CSharp.dll,与com组件操作还会用到System.Dynamic.dll)

 

作者:韩子卢