引用类型和值类型, 是一个老生常谈的问题了. 装箱拆箱相信也是猿猿都知, 但是还是跟着 CLR via C# 加深下印象, 看有没有什么更加根本和以前被忽略的知识点.
引用类型:
引用类型有哪些这里不过多赘述, 来关心一下它在计算机内部的实际操作, 引用类型总是从托管堆分配, 线程栈上存储的是指向堆上数据的引用地址, 首先确立一下四个事实:
内存必须从托管堆分配
堆上分配成员时, CLR 要求你必须有一些额外成员 (比如同步块索引, 类型对象指针). 这些成员必须初始化.
对象中的其他字节总是设为零
从托管堆上分配对象时, 可能强制执行一次垃圾回收
所以引用类型对性能是有显著影响的.
值类型:
值类型是 CLR 提供的轻量级类型, 它把实际的字段存储在线程栈上
值类型不受垃圾回收器的限制, 所以它的存在缓解了托管堆的压力, 也减少了垃圾回收的次数.
值类型都是派生自 System.ValueType
所有值类型都是隐式密封的, 目的是防止将值类型作为其他引用类型的基类
值类型初始化为空时, 默认为 0, 它不像引用类型是指针, 它不会抛出 NullReferenceException 异常, CLR 还为值类型提供了可控类型.
误区防范: 根据我自己的经验, 要避免对引用类型值类型赋值的错误认识, 我们先需要清楚, 定义值类型, 引用类型的底层实际操作, 下面先根据流程图了解一下:
例子:
- class SomeRef{public int x;}
- struct SomeVal{public int x;}
- staic void Test
- {
- SomeRef r1=new SomeRef();
- SomeVal v1 =new SomeVal();
- r1.x=5;
- v1.x=5;
- SomeRef r2=r1;
- SomeVal v2 =v1;
- r1.x=8;
- v1.x=9;
- string a="QWER";
- string b=a;
- a="TYUI";
- }
这样类似的例子, 相信只要讲到引用类型, 值类型, 就一定会见到, 继续复习一下.
首先揭晓几轮复制后的结构: 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# 这两个实例对装箱拆箱的理解非常有帮助:
- internal struct Point : IComparable
- {
- private Int32 m_x,m_y;
- public Point(int x,int y)
- {
- m_x = x;
- m_y = y;
- }
- public override string ToString()
- {
- return String.Format("({0},{1})", m_x.ToString(), m_y.ToString());
- }
- public int CompareTo(Point p)
- {
- 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));
- }
- public int CompareTo(object obj)
- {
- if (GetType() != obj.GetType())
- {
- throw new ArgumentException("o is not a point");
- }
- return CompareTo((Point)obj);
- }
- }
- static void Main(string[] args)
- {
- // 在栈上创建两个实例
- Point p1 = new Point(10,10);
- Point p2 = new Point(10,20);
- // 调用 Tostring 不装箱
- Console.WriteLine(p1.ToString());
- // 调用非虚方法 GetType 装箱
- Console.WriteLine(p1.GetType());
- // 调用 CompareTo, 不装箱
- Console.WriteLine(p1.CompareTo(p2));
- //p1 装箱
- IComparable C = p1;
- Console.WriteLine(C.GetType());
- // 不装箱, 调用的 CompareTo(object)
- Console.WriteLine(p1.CompareTo(C));
- // 不装箱, 调用的 CompareTo(object)
- Console.WriteLine(p1.CompareTo(p2));
- Console.ReadKey();
- }
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 在托管堆上也有对应的方法, 也不会有装箱发生.
- internal struct point
- {
- private int m_x,m_y;
- pulic point(int x,int y)
- {
- m_x=x;
- m_y=y;
- }
- public void change(int x,int y)
- {
- m_x=x;
- m_y=y;
- }
- public ovveride String ToString()
- {
- return String.Format("{0},{1}",m_x.ToString.m_y.ToString());
- }
- }
- public static void Main()
- {
- Point p = new Point(1,1);
- Console.WriteLine(p);
- p.Change(2,2);
- Console.WriteLine(p);
- Object o=p;
- Console.WriteLine(o);
- ((Point) o).Change(3,3);
- Console.WriteLine(o);
- }
结果: 当然是 (1,1)(2,2) (2,2) (2,2) 前面三次的结果很好理解, 第四次为什么是 (2,2), 因为 object 没有 change 方法, 它等拆箱拆到线程栈新的地址上, 于是后面的操作则是在线程栈上进行, 对 o 堆上的内容没有任何影响
- internale interface IChangeBoxedPoint
- {
- void Change(int x,int y);
- }
- internal struct point
- {
- private int m_x,m_y;
- pulic point(int x,int y)
- {
- m_x=x;
- m_y=y;
- }
- public void change(int x,int y)
- {
- m_x=x;
- m_y=y;
- }
- public ovveride String ToString()
- {
- return String.Format("{0},{1}",m_x.ToString.m_y.ToString());
- }
- }
- public static void Main()
- {
- Point p =new p(1,1);
- Console.WriteLine(p);
- p.Change(2,2);
- Console.WriteLine(p);
- Objec o =p;
- Console.WriteLine(o);
- ((Point) o).Change(3,3);
- Console.WriteLine(o);
- ((IChangeBoxedPoint) p).Change(4,4);
- Console.WriteLine(p);
- ((IChangeBoxedPoint) o).Change(5,5);
- Console.WriteLine(o);
- }
结果: 前面四次的结果应该是显而易见了,(1,1)(2,2) (2,2) (2,2), 那么第五次呢, 来简单分析一下 p 装箱为 IChangeBoxedPoint, 然后把堆上对应的 p 的 m_x,m_y 改为 4,4, 但是对 p 输出时堆上的内容不仅回收了, 而且输出的是原来 p 线程栈上的内筒, 仍然还是刚刚的 (2,2), 第六步, o 没有任何装箱拆箱操作, 当然是预期的 (5,5)
来源: https://www.cnblogs.com/franhome/p/8901008.html