引用类型和值类型,是一个老生常谈的问题了。装箱拆箱相信也是猿猿都知,可是仍是跟着CLR via C#加深下印象,看有没有什么更加根本和之前被忽略的知识点。数组
引用类型:ide
引用类型有哪些这里不过多赘述,来关心一下它在计算机内部的实际操做,引用类型老是从托管堆分配,线程栈上存储的是指向堆上数据的引用地址,首先确立一下四个事实:性能
内存必须从托管堆分配spa
堆上分配成员时,CLR要求你必须有一些额外成员(好比同步块索引,类型对象指针)。这些成员必须初始化。线程
对象中的其余字节老是设为零指针
从托管堆上分配对象时,可能强制执行一次垃圾回收code
因此引用类型对性能是有显著影响的。orm
值类型:对象
值类型是CLR提供的轻量级类型,它把实际的字段存储在线程栈上blog
值类型不受垃圾回收器的限制,因此它的存在缓解了托管堆的压力,也减小了垃圾回收的次数。
值类型都是派生自System.ValueType
全部值类型都是隐式密封的,目的是防止将值类型做为其余引用类型的基类
值类型初始化为空时,默认为0,它不像引用类型是指针,它不会抛出NullReferenceException异常,CLR还为值类型提供了可控类型。
误区防范:根据我本身的经验,要避免对引用类型值类型赋值的错误认识,咱们先须要清楚,定义值类型,引用类型的底层实际操做,下面先根据流程图了解一下:
例子:
1 class SomeRef{public int x;} 2 struct SomeVal{public int x;} 3 4 staic void Test 5 { 6 SomeRef r1=new SomeRef(); 7 SomeVal v1 =new SomeVal(); 8 9 r1.x=5; 10 v1.x=5; 11 12 SomeRef r2=r1; 13 SomeVal v2 =v1; 14 r1.x=8; 15 v1.x=9; 16 17 string a="QWER"; 18 string b=a; 19 a="TYUI"; 20 }
这样相似的例子,相信只要讲到引用类型,值类型,就必定会见到,继续复习一下。
首先揭晓几轮复制后的结构:r1.x=8,r2.x=8 v1.x=9 v2.x=5 a="TYUI" b="QWER"
简单分析一下:
r1 ,r2在线程栈上存储的是同一个指向内存堆的地址,当r1值改变时,实际上是直接改变内存堆里的内容,天然r1,r2所有变成了8。
而v1,v2是独立存储在线程栈上的,v1值改变时,只是单单改变v1线程栈里的值,天然v2=5,v1=9。
而a,b的值为何不像上面r1.x同样变化呢,它们不是引用类型吗,这就须要去看看上面的流程图,由于你在给a改变赋值时,实际上是在托管堆上开辟了一个新的空间,你传给a的是一个新的地址,而b还指向原来的老地址。
结合上面的三个图和示例,对于引用类型和值类型构建相信应该有一个清楚的理解了。
使用值类型的一些建议:
值类型相对于引用类型,性能上更有优点,可是考虑在业务上的问题,值类型通常须要知足下面的所有条件,才是适合定义为值类型:
类型具备基元类型的行为。也就是说,是十分简单的类型,没有成员会修改类型的任何实例。若是类型没有提供会更改其余字段的成员,就称为不可变类型(immutable)。事实上,对于许多值类型,咱们都建议将所有字段标记为readonly。
类型不须要从其余类型继承
类型不派生出其余类(隐式密封)。
类型大小也应考虑:
由于实参默认以传值方式传递,形成对值类型实例中的字段进行复制,若是值类型过于大会对性能形成损害。
一样,当顶一个值类型的方法返回时,实例中的字段会复制到调用者分配的内存,也可能形成性能的损害。
因此,必须知足如下任意条件:
类型实例较小(16字节或更小)
类型实例较大(大于16字节),但不做为方法实参传递,也不从方法传递
值类型的局限:
值类型有两种形式:未装箱和已装箱,而引用类型一直是已装箱。
值类型从System.ValueType派生,System.ValueType重写了Equals和GetHashCode方法。生成哈希码时,会将对象的实例字段的值考虑在内。因此定义本身的值类型时,因重写Equals和GetHashCode方法。
值类型不能被继承,它本身的方法不能是抽象的,全部都是隐式密封的。
值类型不在内存堆中分配,因此一个实例的方法再也不活动时,分配给值类型的内存空间会被释放,而没有垃圾回收机制来处理它。
值类型的装箱拆箱:
例如,ArrayList不断的添加值类型进入数组时,就会发生不断的装箱操做,由于它的Add方法参数是object类型,天然装箱就不可避免,天然也会形成性能的损失(FCL如今提供了泛型集合类,System.Collection.Generic.List<T>,它不须要装箱拆箱操做。使得性能提高很多)。
装箱相关的含义相信不用过多解释,咱们来关心一下,内存中的变化,看看它是如何对性能形成影响的。
装箱:
在托管堆中分配内存。内存大小时值类型各字段所需的内存加上两个额外成员(托管堆全部对象都有)类型对象指针和同步块索引所需的内存量。
值类型的字段值复制到堆内存的空间中。
返回堆上对应的地址
而后,一个值类型就变成了引用类型。
拆箱:
根据引用类型的地址找到堆内存上的值
将值复制给值类型
拆箱的代价比装箱小得多
装箱拆箱注意点:
下面经过几个示例,来熟悉一下装箱拆箱的过程,并学会如何避免错误的断定装箱拆箱,CLR via C#这两个实例对装箱拆箱的理解很是有帮助:
1 internal struct Point : IComparable 2 { 3 private Int32 m_x,m_y; 4 public Point(int x,int y) 5 { 6 m_x = x; 7 m_y = y; 8 } 9 10 public override string ToString() 11 { 12 return String.Format("({0},{1})", m_x.ToString(), m_y.ToString()); 13 } 14 15 16 public int CompareTo(Point p) 17 { 18 return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y) - Math.Sqrt(p.m_x * p.m_x + p.m_y * p.m_y)); 19 } 20 21 public int CompareTo(object obj) 22 { 23 if (GetType() != obj.GetType()) 24 { 25 throw new ArgumentException("o is not a point"); 26 } 27 return CompareTo((Point)obj); 28 } 29 }
1 static void Main(string[] args) 2 { 3 //在栈上建立两个实例 4 Point p1 = new Point(10,10); 5 Point p2 = new Point(10,20); 6 7 //调用Tostring不装箱 8 Console.WriteLine(p1.ToString()); 9 10 //调用非虚方法GetType装箱 11 Console.WriteLine(p1.GetType()); 12 13 //调用CompareTo,不装箱 14 Console.WriteLine(p1.CompareTo(p2)); 15 16 //p1装箱 17 IComparable C = p1; 18 Console.WriteLine(C.GetType()); 19 20 //不装箱,调用的CompareTo(object) 21 Console.WriteLine(p1.CompareTo(C)); 22 23 //不装箱,调用的CompareTo(object) 24 Console.WriteLine(p1.CompareTo(p2)); 26 27 Console.ReadKey(); 28 }
1.调用ToString
不装箱,由于ToString是从ValueType继承的虚方法,中间没有类型转换的发生,不须要进行装箱,另外注意的是:Equals,GetHashCode,ToString都是从ValueTye继承的虚方法,因为值类型都是密封类,没法派生,因此只要你的值类型重写了这些方法,并无去调用基类的实现,那么是不会发生装箱的,若是你去调用基类的实现,或者你没有实现这些方法,那么仍是可能发生装箱。
2.调用GetType
GetType是继承自Object,而且不能被重写,因此不管如何值类型对其调用都会发生装箱,另外MemberwiseClone方法也是如此。
3.第一次调用CompareTo方法
由于Point里面有了类型为Point的参数CompareTo方法,不会发生装箱操做
4.p1转换为ICompable
确认过眼神,这必定是一个装箱。
5.第二次调用CompareTo方法
虽然此次调用的是参数为object的方法,可是注意的是:首先咱们Point实现了这个重载,另外传进去的是个ICompable,天然不会发生装箱(另外,若是Point自己没有这个方法呢?固然会装箱,由于它不得不去调用父类的方法,而父类是一个引用类型,天然须要进行一次装箱操做)
6.第三次调用CompareTo方法
c是ICompable,而ICompable在托管堆上也有对应的方法,也不会有装箱发生。
5 internal struct point 6 { 7 private int m_x,m_y; 8 9 pulic point(int x,int y) 10 { 11 m_x=x; 12 m_y=y; 13 } 14 15 public void change(int x,int y) 16 { 17 m_x=x; 18 m_y=y; 19 } 20 21 public ovveride String ToString() 22 { 23 return String.Format("{0},{1}",m_x.ToString.m_y.ToString()); 24 } 25 26 }
1 public static void Main() 2 { 3 Point p = new Point(1,1); 4 Console.WriteLine(p); 5 6 p.Change(2,2); 7 Console.WriteLine(p); 8 9 Object o=p; 10 Console.WriteLine(o); 11 12 ((Point) o).Change(3,3); 13 Console.WriteLine(o); 14 }
结果:固然是 (1,1)(2,2) (2,2) (2,2) 前面三次的结果很好理解,第四次为何是(2,2),由于object没有change方法,它等拆箱拆到线程栈新的地址上,因而后面的操做则是在线程栈上进行,对o堆上的内容没有任何影响
1 internale interface IChangeBoxedPoint 2 { 3 void Change(int x,int y); 4 } 5 internal struct point 6 { 7 private int m_x,m_y; 8 9 pulic point(int x,int y) 10 { 11 m_x=x; 12 m_y=y; 13 } 14 15 public void change(int x,int y) 16 { 17 m_x=x; 18 m_y=y; 19 } 20 21 public ovveride String ToString() 22 { 23 return String.Format("{0},{1}",m_x.ToString.m_y.ToString()); 24 } 25 26 }
1 public static void Main() 2 { 3 Point p =new p(1,1); 4 Console.WriteLine(p); 5 6 p.Change(2,2); 7 Console.WriteLine(p); 8 9 Objec o =p; 10 Console.WriteLine(o); 11 12 ((Point) o).Change(3,3); 13 Console.WriteLine(o); 14 15 ((IChangeBoxedPoint) p).Change(4,4); 16 Console.WriteLine(p); 17 18 ((IChangeBoxedPoint) o).Change(5,5); 19 Console.WriteLine(o); 20 }
结果:前面四次的结果应该是显而易见了,(1,1)(2,2) (2,2) (2,2),那么第五次呢,来简单分析一下p装箱为IChangeBoxedPoint,而后把堆上对应的p的m_x,m_y改成4,4,可是对p输出时堆上的内容不只回收了,并且输出的是原来p线程栈上的内筒,仍然仍是刚刚的(2,2),第六步,o没有任何装箱拆箱操做,固然是预期的(5,5)