谈一谈值类型与引用类型和装箱与拆箱

哇。。。  明天就放假了,很开心,原本每周五晚上都是去看电影的,结果今天要陪老婆去精英学英语, 漫长的三个多小时,还好不是让我光等着,给了我一台电脑让我上上网,因而决定继续分享一些我所理解的C#知识。先写理论的东西,等回家了在补实例(没办法啊,机子上没有vs,咋也不能我给人家装一个不是。)程序员

我写博客的目的:面试

第一点也是我写博客最重要的一点,就是经过把本身所理解技术写下来,以巩固本身学习的知识(可能不像其余园友那样只是单纯的为了和你们分享本身的技术。。。嘿嘿)。由于本身是学数学专业的,去年三月份刚刚接触编程这么个东西,知识不像计算机专业的同窗那么系统,所以也想经过写博客来记录本身学习的知识,未来回过头来翻看。编程

第二点分享我所理解的给你们,从而但愿对读者有必定的帮助。(这一点确定是有的,^_^)。数组

第三点就是也能经过通园友的探讨和批评建议中提升本身。因此但愿园友还有各路大神们留下宝贵的墨笔,小子在此感激涕零。数据结构

 

言归正文,前几天面试(嘿嘿,是咱们技术老大去面,我属于旁听的,基本没我什么事,不过真的能学到不少东西),碰到一个小伙伴,公司面试题中有一个题目,就是问什么是值类型与引用类型。小伙伴回答的很完整:ide

C#的值类型包括:结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型。

C#的引用类型包括:数组,用户定义的类、接口、委托,object,字符串。

数组的元素,不论是引用类型仍是值类型,都存储在托管堆上。

引用类型在栈中存储一个引用,其实际的存储位置位于托管堆。为了方便,本文简称引用类型部署在托管推上。

值类型老是分配在它声明的地方:做为字段时,跟随其所属的变量(实例)存储;做为局部变量时,存储在栈上。

这是我在百度上摘下来的,他回答的和这些差很少,基本都答出来了,而后老大拿着面试题,以为他这个写的不错,就又问了问,说你以为在何时或者状况下适合用值类型,什么状况下适合用引用类型啊? 结果小伙伴哑口无言。。。函数

额。。。 我以为这中状况的小伙伴确定非长多,我也是,当初为了应付找工做,不少状况下都是在背面试题。好比什么是接口,而后上网上一搜:性能

     而后看都有人回答:学习

接着,背诵熟悉了,而后去面试,反正当初的我就是这样的。。。。。。spa

但是,这样就真的了解接口和抽象类了吗?

答案是NO,就算你把接口抽象类的概念背的再熟悉,你就这的理解面向接口,面向抽象了吗?  就算你把继承、封装、多态的概念滚瓜烂熟,你就真的了解面向对象思想了吗。。。我以为,这些概念是为了让咱们懂得怎么去更高的去应用。这才这最主要的 。

 

回到今天的主题上来。

CRL支持两种类型,一种是值类型,一种是引用类型,在FCL中,大部分的类型都是引用类型,引用类型的对象老是从托管堆中分配,C#用new关键字来返回对象的内存地址。也就是说,当你要用一个引用类型时,你要考虑到一下几点。

  1--要从托管对上分配内存

  2--托管堆中的每一个对象都会有一些额外的成员:类型对象指针和同步快索引

    类型对象指针:就是指向对象的类型对象的指针,额。。。 估计这么解释确定听不懂,反正我当时接触的时候是听不懂的。其实就是这个意思,咱们知道咱们写的C#代码,在编译的时候都被C#编译器编译成一个托管模块,其中包括CLR头,PE头,还有IL代码 和元数据,IL(中间语言)就是咱们所说的托管代码,他跑在CRL上面的,当应用程序运行的时候,为了执行一个方法,首先必须把IL代码转换成本地的CPU语言,这时候就用到JIT(即时编译器),在运行一个方法以前,CLR会检测这个方法里面所用到的全部的类型,而后为这些类型在托管堆中建立一个数据结构,也就是类型对象,好比所你在方法里面用到了Student类,那么就会建立Student的类型对象,当你new一个Student的实例的时候,又会在托管堆中建立一个Student对象的实例,这个对象实例里面包含的类型对象指针就这行Student这个类型对象。咱们说托管对中的每一个对象都有类型对象指针和同步快索引,Student类型对象也不例外,他的类型对象指针指向System.Type类型对象(这个就是祖宗了,就像当与object是全部类型的祖宗同样),System.Type类型对象也是一个对象,也包含类型对象指针,他指向他本身。  - - - - - -不知道我这样解释能明白不。。。

    同步快索引:这个没咋研究过,不过他在CLR里面是一个和牛逼的人物,挺多的功能都要经过它实现,等之后有机会再研究。

  3--没当在托管堆中建立一个对象的时候,都有可能会强制的执行一次垃圾回收。为啥说可能呢,我以为应该是这么回事,垃圾回收器中不是有“代”这个概念吗,当要在托管堆中建立对象时,发现最低代满了的时候,就会执行一次垃圾回收。(我猜的啊,不过我以为           是这样)

你看啊,弄一个引用类型的对象多麻烦啊,若是全部的类型都用引用类型的话,那么程序的性能就会降低,若是没建立一个int对象,都要在托管对中分配内存,这样性能会受很大的影响,因此CRL又支持一种“轻量级的”类型:值类型。

值类型的实例通常都是在堆栈上分配的,这里用的是通常状况啊,由于有时候可能作为应用类型的对象的字段存放在对象中。如今我们就说这个通常的状况下。在表明值类型的变量当中,不包含值类型实例的指针,而是包含实例对象自己,这样就不须要再托管对中分配内存,这样一来,值类型的使用,即减轻了托管对的压力,同时又较少了一个应用程序的一个生命周期内,垃圾回收的次数。

 

在FCL中,大多数类型都是引用类型,或者就是通常管引用类型都叫“类”,好比system.Console类 Match类 还有什么接口啊 事件 委托 数组都是引用类型,而值类型,在FCL中通常都叫结构或者枚举。例如system.Int32结构,System.DayAndWeek枚举。

全部的结构都是从System.ValueType派生出来的,全部的枚举都是从System.Enum类派生出来的,System.Enum又是从System.ValueType派生的。并且全部的值类型都是sealed(密封)的

 

说了这么多理论性的东西,不少小伙伴都改烦了,好吧,我用代码展现一下值类型和引用类型在某些方面的不一样。

1     public struct CalValue  //结构:属于值类型
2     {
3         public int age;
4     }
6     public class CalRef  //类:输入引用类型
7     {
8         public int age;
9     }

在Main函数里:

 1             CalValue val = new CalValue(); //在线程栈上分配 此时val变量包含着CalValue的实例
 2             val.age = 10;//在线程栈上直接修改
 3             CalValue val1 = val;//在线程栈上分配而且复制成员,
 4             val1.age = 20;//在线程栈上修改,此时只修改val1.age 而 val.age不会修改,由于这是两个彻底独立的实例
 5             CalRef re = new CalRef();//在对上分配,关键字new调用CalRef类的构造函数,返回建立的对象的地址,保存在变量re中。
 6             re.age = 10;//提领指针,找到对象而后修改对象的age字段
 7             CalRef re1 = re;//定义变量re1,而且将re内保存的对象指针复制到re1中,此时,re和re1变量同时包含了指向同一个对象的地址。
8 re1.age = 20;//提领指针,找到对象而后修改对象的age字段,因为re和re1变量内保存的指针指向同一个对象,因此re.age也会改变为 20
 9  Console.WriteLine(val.age); 10  Console.WriteLine(val1.age); 11  Console.WriteLine(re.age); 12 Console.WriteLine(re1.age); 显示结果10 20 20 20

我想你们看了上面代码里面的注释就已经很明白了吧,让咱们看看它编译成的IL:

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] valuetype ValueTypeAndObjectType.CalValue val,
        [1] valuetype ValueTypeAndObjectType.CalValue val1,
        [2] class ValueTypeAndObjectType.CalRef re,
        [3] class ValueTypeAndObjectType.CalRef re1)  //表示方法内部定义了四个局部变量,前面的数字是变量的索引
    L_0000: nop //no operation 没操做,我也不知道为啥没操做为啥还要写上这么个东西,正在研究中。。。
------------------------------------------------
   CalValue val = new CalValeu(): L_0001: ldloca.s val //将变量val的地址压入栈中 L_0003: initobj ValueTypeAndObjectType.CalValue // initobj表示建立一个值类型 并经过val的变量地址保存在val变量中("
并经过val的变量地址保存在val变量中",是我猜测的,但我绝对应该是这样了,你们能够作个参考,而后查看相关资料,知道是咋回事的大神还但愿您指点一二,再猜测拜上了哈)
--------------------------------------------------
   val.age = 10 L_0009: ldloca.s val //
压入val变量的地址 L_000b: ldc.i4.10 //将常量 10 压入栈中
  L_000d: stfld int32 ValueTypeAndObjectType.CalValue::age // 将10保存在 val中的实例的age字段中
--------------------------------------------------
   CalValue val1 = val;
   val1.age = 20; L_0012: ldloc.
0 //
将索引为0的局部变量装在到栈中,这里就是变量val L_0013: stloc.1 // 吧栈中返回的值存放到索引为1的局部变量中,这里就是至关于把val变量整个复制了一份,而后赋给val1,由于val是值类型变量,里面包含了一个值类型实例,因此val1里面也就有了一个值类型实例 L_0014: ldloca.s val1 //同上 L_0016: ldc.i4.s 20 //同上 L_0018: stfld int32 ValueTypeAndObjectType.CalValue::age // 此时是将20赋给了val1里面实例的age字段。
--------------------------------------------------
   CalRef re = new CalRef(); L_001d: newobj instance
void ValueTypeAndObjectType.CalRef::.ctor() //newobj表示建立引用类型对象,并返回一个对象的地址 L_0022: stloc.2 //将地址存储在索引为2的变量中 :re
-------------------------------------------------- L_0023: ldloc.
2 //将变量re 装载到栈中 L_0024: ldc.i4.s 10 L_0026: stfld int32 ValueTypeAndObjectType.CalRef::age //将10赋值给对象的age字段
--------------------------------------------------
   Calref re1 = re; L_002b: ldloc.
2 L_002c: stloc.3 //这里是关键,在这里能够看出,知识把re变量整个赋值一下,而后给re1,而没有将堆中的对象也同时复制。由于re里面包含类对象的指针,因此re1里面也包含了一个相同的指针(由于是复制嘛),这两个指针指向同一个对象。(看到了IL代码,我才更加理解只是复制了引用是怎么回事)
-------------------------------------------------- L_002d: ldloc.
3 L_002e: ldc.i4.s 20 L_0030: stfld int32 ValueTypeAndObjectType.CalRef::age L_0035: ldloca.s val L_0037: ldfld int32 ValueTypeAndObjectType.CalValue::age L_003c: call void [mscorlib]System.Console::WriteLine(int32) L_0041: nop L_0042: ldloca.s val1 L_0044: ldfld int32 ValueTypeAndObjectType.CalValue::age L_0049: call void [mscorlib]System.Console::WriteLine(int32) L_004e: nop L_004f: ldloc.2 L_0050: ldfld int32 ValueTypeAndObjectType.CalRef::age L_0055: call void [mscorlib]System.Console::WriteLine(int32) L_005a: nop L_005b: ldloc.3 L_005c: ldfld int32 ValueTypeAndObjectType.CalRef::age L_0061: call void [mscorlib]System.Console::WriteLine(int32) L_0066: nop L_0067: ret }

我相信你们看了IL代码以后是否是就更加理解这个问题了。

 

这里我总结一下值类型好引用类型的区别:

1---从表示形式来看:值类型有两种,一种是未装箱形式,一种是已装箱形式。而引用类型都是已装箱的形式。

2---从实例化的角度上看,值类型实例化,不须要在托管对中非配内存。他与他的变量一块儿保存在堆栈上。而引用该类型实例化时候要在托管堆中分配内存,而后要先初始化对象的两个额外成员,而后经过关键字new给调用构造函数,建立对象并返回对象的地址保存在

变量中。这时变量只保存了对象的地址。

3---从性能方面看,值类型要优与引用类型,由于值类型不须要在堆上分配内存,因此也涉及不到垃圾回收。这样既缓解了托管堆的压力,又减小了应用程序一个生命周期内的垃圾回收次数。 这里说一下,对于值类型,一旦定义了他的实例的方法不在处于活动状态,那

么为这些实例分配的内存就会释放。

4---从赋值角度上看(额。。。我也不知道这个角度该叫啥,暂且就这么叫着吧),因为值类型的实例是直接保存在变量中的,而引用类型的实例保存在托管堆中,因此变量赋值的时候,值类型会执行一次逐字段的赋值,而引用类型只是赋值地址。

5---因为第4条,多个引用类型的变量能够引用托管堆中同一个对象,所以其中一个变量执行的操做会影响到其余变量的对象。而值类型则不会。

 

值了行虽然好,单不是何时都能用的,下面列出一些值类型的条件,最好只有如下条件都知足的时候,再用值类型:

1---类型很是简单,其中没有成员能修改任何其实力字段,事实上,许多值类型,建议把他的全部子都设置为只读的(readonly)

2---类型须要从其余任何类型继承

3---类型也不会派生出任何其余类型 (由于值类型适合作数据的载体,不支持多态,这种事是类干的事,类用来定义应用程序的行为) 

4---若是值类型的实例要被做为参数传递的话,那么这个值类型的实例应该较小,由于默认的状况下,实参是以值传递的形式传递的,这会形成对实参进行复制。(大小约为16字节或者更小)---这是Jeffery Richter说的(在编程这个领域里是个人偶像啊 嘿嘿)。对了这

里只是说默认的状况下,由于有时候会由于ref或者out而改变。

5---值类型实例能够很大,可是不能做为参数传递。

 

好了,今天就写到这吧,说实话写这篇挺累的,花了我近三个多小时,要不是明天放假。。。。,并且还特娘的没完,好比装箱拆箱都没说,哎有时间下一节的随笔里面在说吧。

哈哈明天是俺的生日了,额。。。不 ,是今天。。。   一会洗个澡好好睡一觉。明天得和那帮铁子出去疯一天去,并且老婆说有惊喜给我,期待啊。。。

最后,我仍是想说我老师说的,程序员是搞艺术的,他们不是苦逼,而是艺术家。^_^

好啦 睡觉 你们~安啦!

相关文章
相关标签/搜索