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

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

我写博客的目的:

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

第二点分享我所理解的给大家,从而希望对读者有一定的帮助。(这一点肯定是有的,^_^)。

第三点就是也能通过通园友的探讨和批评建议中提高自己。所以希望园友还有各路大神们留下宝贵的墨笔,小子在此感激不尽。

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

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

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

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

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

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

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

额。。。 我觉得这中情况的小伙伴肯定非长多,我也是,当初为了应付找工作,很多情况下都是在背面试题。比如什么是接口,然后上网上一搜:

     然后看都有人回答:

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

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

答案是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---值类型实例可以很大,但是不能作为参数传递。

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

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

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

好啦 睡觉 大家~安啦!

原文地址:https://www.cnblogs.com/feng-c-x/p/3320598.html