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

 

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

我写博客的目的:

 

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

 

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

 

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

 

 

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

 

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

 

 

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

 

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

 

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

mono ios莫名其妙闪退的解决方法

 

使用mono进行ios开发也有一年了,一直有个头疼的问题是闪退,而且闪退的时候并没有抛出明确的错误。

前两天在调试一个bug的时候,在序列化的时候又莫名其妙的闪退,后来在一位大神(博客地址)的指导下,发现了解决方案!

遇到这种闪退,一般在Application output中输出错误如下:

复制代码
    ……………………
    0x01e394ac monoeg_g_log + 208 6 TrackAboutIOS
    0x01d11664 get_numerous_trampoline + 160 7 TrackAboutIOS
    ……………………
    =================================================================
          Got a SIGSEGV while executing native code. This usually indicates
           a fatal error in the mono runtime or one of the native libraries 
          used by your application.
   =================================================================
复制代码

而且这种错误是随机的,有时候正常运行,有时候不正常,在上文输出错误内容里我们看到trampoline这个单词,而第一句monoeg_g_log 这句话是说系统在尝试记录错误到错误日志,基于这种情况下,我们可以判定这是mono的默认蹦床(trampoline)数小于了你应用需要的蹦床数。

关于蹦床数的解释,这里Rolf Bjarne Kvinge给了相应的解释:相应链接

复制代码
On device we generate all the necessary code at build time in a process known as Ahead of Time compilation (similar to Microsoft's ngen), because we're not allowed to jit code on devices. Unfortunately there are a few things that cannot be determined statically - for instance generic interfaces might need different vtables depending on which type the interface is instantiated with. (For this case it is technically possible to determine the maximum number of vtables, but the number would be potentially enormous - multiply the number of generic interfaces times the number of types in your app...). We cannot allocate memory for these vtables dynamically at runtime, so we've picked a reasonable default and allow the user to increase this value if they run into issues. This is the basic theory for the trampolines (the exact problem is a bit different, depending on the type of trampolines, but that's not really important).

So you can add as many trampolines as you want, but memory usage will increase. That's also all there is to it: the app will not get slower (unless if the increased memory usage causes it to run slower, due to out-of-memory warnings, etc). It also means that you only have to increase the number of trampolines of the type you're actually having problems with, if you increase the others you'll increase the size of your executable needlessly.
复制代码
 

大体意思是(英语不大好,尽力翻译了):

复制代码
因为mono不允许在苹果设备上即时编译代码,所以在编译的时候mono会通过AOT编译技术直接编译为ARM汇编代码。但在编译的时候仍有一些无法静态确定的事情:例如泛型接口可能需要不同的虚拟表(运行时存放执行方法的集合),这取决于接口被实例化时的类型。(对于这种情况,从技术上讲确定虚拟表的最大数量是可能的,但是这数量可能会是庞大的--即泛型接口数乘以应用中类型数)……我们无法为这些虚拟表在运行时动态的分配内存,所以我们指定了一个合理的默认值并且允许用户在运行时出现问题的情况下增加默认值。这就是蹦床的基本理论(实际情况略微不同,这依赖于蹦床数的类型,但这并不重要)

所以你可以按你的需要尽可能的增加蹦床数,但是内存的使用会增加。 应用一般不会变慢(除非内存的使用增多导致了内存不足,从而引起运行变慢)。这也同时意味着你只仅仅需要增加发生问题那块对应的蹦床类型的数目。如果你增加了其他蹦床类型的数目,你也会不必要的增多了执行的体积。
复制代码

看完这个,我想你对蹦床数有了一定了解,那如何设置呢!

打开xamarin studio ,在项目文件上右键option,展开下图,在arguments参数的地方,输入:-aot "nrgctx-trampolines=4096" -aot "nimt-trampolines=4096" -aot "ntrampolines=4096"

请看下图:

QQ图片20130906181222

我们在上面看到了-aot "nrgctx-trampolines=4096" -aot "nimt-trampolines=4096" -aot "ntrampolines=4096"这几个输入参数,那这几个参数相对应的意思是什么呢?

这里官网给了我们解释:链接地址

复制代码
Ran out of trampolines of type 0
If you get this message while running device,  You can create more type 0 trampolines (type SPECIFIC) by modifying your project options "iPhone Build" section.  You want to add extra arguments for the Device build targets:

-aot "ntrampolines=2048"

The default number of trampolines is 1024.  Try increasing this number until you have enough for your application.

Ran out of trampolines of type 1
If you make heavy use of recursive generics, you may get this message on device.  You can create more type 1 trampolines (type RGCTX) by modifying your project options "iPhone Build" section.  You want to add extra arguments for the Device build targets:

-aot "nrgctx-trampolines=2048"

The default number of trampolines is 1024.  Try increasing this number until you have enough for your usage of generics.

Ran out of trampolines of type 2
If you make heavy use interfaces, you may get this message on device.  You can create more type 2 trampolines (type IMT Thunks) by modifying your project options "iPhone Build" section.  You want to add extra arguments for the Device build targets:

-aot "nimt-trampolines=512"

The default number of IMT Thunk trampolines is 128.  Try increasing this number until you have enough for your usage of interfaces.
复制代码

下面进行翻译一下:

复制代码
Ran out of trampolines of type 0
如果你再运行时,输出这个错误,你可以在项目option--iphone build处添加额外的参数-aot "ntrampolines=2048"
这个参数默认值为1024,试着增加这个值直到满足你的应用的需求。
Ran out of trampolines of type 1
如果你使用了过多的泛型嵌套,如List<T>中还有List<T>成员,你可以同上通过添加额外的参数-aot "nrgctx-trampolines=2048" 来解决。
蹦床类型1的默认值为1024.
Ran out of trampolines of type 2
如果你界面操作频繁,你可以通过添加额外的参数-aot "nimt-trampolines=512" 来解决。
这里的默认值为128.


复制代码
 
 
分类: Mono
原文地址:https://www.cnblogs.com/Leo_wl/p/3322303.html