• CLR-2-2-引用类型和值类型


    引用类型和值类型,是一个老生常谈的问题了。装箱拆箱相信也是猿猿都知,但是还是跟着CLR via C#加深下印象,看有没有什么更加根本和以前被忽略的知识点。

     

    引用类型:

    引用类型有哪些这里不过多赘述,来关心一下它在计算机内部的实际操作,引用类型总是从托管堆分配,线程栈上存储的是指向堆上数据的引用地址,首先确立一下四个事实:

     内存必须从托管堆分配

     堆上分配成员时,CLR要求你必须有一些额外成员(比如同步块索引,类型对象指针)。这些成员必须初始化。

     对象中的其他字节总是设为零

     从托管堆上分配对象时,可能强制执行一次垃圾回收

    所以引用类型对性能是有显著影响的。

     

    值类型:

    值类型是CLR提供的轻量级类型,它把实际的字段存储在线程栈上

    值类型不受垃圾回收器的限制,所以它的存在缓解了托管堆的压力,也减少了垃圾回收的次数。

    值类型都是派生自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)

  • 相关阅读:
    台湾9大知名开源社区
    使用SignalR打造消息总线
    ENode 2.0
    Wireshark基本介绍和学习TCP三次握手 专题
    linux tomcat 的安装
    linux 之静默安装oracle
    什么是全栈呢(转)
    Android开发自学笔记(基于Android Studio1.3.1)—1.环境搭建(转)
    hdu 4919 Exclusive or
    D
  • 原文地址:https://www.cnblogs.com/franhome/p/8901008.html
Copyright © 2020-2023  润新知