理解C#拆箱和装箱以及值类型和引用类型对象

前言:
之前在学c++就了解过值类型和引用类型,今天看了《深入理解c#第三版》以及查阅了大量的资料,总算勉强深入的理解了c#中这些的内涵,在此做个记录,方便自己复习,同时这篇文章如果能够帮到你,本人深感荣幸。
c#数据类型概述
首先c#只有2种数据类型,一个是值类型,一个是引用类型。
提到数据类型就得说存储的地方,在此先把网上经常流传的一个不是全对的结论放在这里,在后面我会解释他为什么是错误的。
“引用类型是保存在堆上,值类型是保存在栈上”。
值类型
c#中简单类型、结构体、枚举类型都是在堆上分配。
简单类型包括 float int doblue bool char 以及什么long int uint等等
引用类型
c#中引用类型 包含string class Arry list object
比如你写了这两个代码:

  class B
        {
            int x;
        }
        struct A
        {
            int i;
        }

然后你生成这两个的实例对象,那么实际内存情况分配如下图:
在这里插入图片描述可以看出生成的结构体对象是在栈区,而类对象是引用类型内存是分配在托管堆上的,在栈上存的只是他的对象在堆区的地址也就是引用而已。而且从这个图我们可以看到,B类中是存在一个值类型int x的,但是这个x却是在堆上,而不是在栈上的。
这就是我之前说的那句话的理由,那句话中——“引用类型是在堆上”是完全没问题的,但是“值类型是在栈上分配”这句话是有问题的,这里就是个例子。这也是个很常见的例子,任何值类型字段只要是类的成员,那么它的分配是在堆上,那么什么时候才是值类型在栈上分配呢,是局部变量,和方法参数才会这样,而且在c#2.0以上的版本中很多的局部变量并不是完全存放在栈。
参数传递
数据类型的另一大区分肯定就是在传递参数的时候,同样先抬出这个网上普遍默认的误区——“对象是在c#中默认通过引用类型传递的”。
我直接上一个代码来看看:

 static  string str1 = null;
       static  void Str(  string str1)
        {
            str1 += "我是中国人";
            Console.WriteLine(str1);
        }
         static  void Main(string[] args)
        {
            str1 = "Hello World";
            Console.WriteLine(str1);
            Str(  str1);
            Console.WriteLine(str1);
            Console.ReadKey();
        }

运行截图如下:
在这里插入图片描述
第二次改动一下代码如下:

static  string str1 = null;
       static  void Str( ref  string str1)
        {
            str1 += "我是中国人";
            Console.WriteLine(str1);
        }
         static  void Main(string[] args)
        {
            str1 = "Hello World";
            Console.WriteLine(str1);
            Str( ref  str1);
            Console.WriteLine(str1);
            Console.ReadKey();
        }

运行截图如下:
在这里插入图片描述
事实胜于雄辩吧,不管你是不是值类型还是引用类型的参数在c#中传递时候,默认都是值类型,只有加上ref的时候,才会变成引用类型。
事实上“引用传递”的正式定义很复杂,但其最最重要的是一点是我们可以通过方法里面更改其参数值而达到更改调用者的变量值,也就是能够更改本身。
所以按照我们上面的测试代码的结果来看,就算是引用类型的变量,你不显示的加ref 他依然更改不了原来的值,而我们在方法体内部,对参数进行输出就发现改变了,这说明其实我们改变的还是“副本”。
各位有兴趣的可以去看看IL是怎么执行的,所以不管是哪种类型参数,默认传递都是值,并不会改变原来的值。
我们简单的捋清楚了值类型和引用类型就来看看装箱拆箱
装箱和拆箱的定义
什么是装箱,装箱就是把值类型变成引用类型的过程,而拆箱就是把引用类型变成值类型的过程。
最简单的代码如下:


            int i = 9;
            object obj = i;
            int x = (int)obj;
            Console.WriteLine(x);

运行截图:
在这里插入图片描述
之前也说了那些是值类型,哪些是引用类型,那么在上面这段代码里,先把i装箱到obj中,再拆箱出来,然后打印出来,和之前定义的一样这就是装箱和拆箱过程。
装箱时候发生的动作
1.在托管堆上分配内存,其内存大小是被装箱的类型的实例大小再加上类型对象指针同步块索引这个两个额外成员,2.然后把栈上的值类型的值复制到分配好的堆内存上,3.然后返回引用类型对象的地址,值类型就变成了引用类型了。
在这里有一个小细节,就是当所需要的堆上的对象实例大小小于85000Byte时候会被分配到GC堆上,大于等于85000Byte时候就会创建在LOH堆(大对象堆)。
拆箱时候发生的动作
拆箱时候发生的并不是装箱的动作反过来做一遍,而是获取到引用对象的指针所指向的内容,然后复制到已经分配好的栈上的值类型内存上,所以拆箱的性能消耗其实比装箱小得多。
装箱和拆箱的性能消耗
上一段我的测试代码:

 List<object> list1 = new List<object>();
            //List<int> list1 = new List<int>();
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for(int i = 0; i < 100000000; i++)
            {
                list1.Add(i);
            }
            sw.Stop();
            Console.WriteLine(sw.Elapsed);

分别用list和list来装int 的时间消耗如下:
在这里插入图片描述
在这里插入图片描述
可以明显看到是10倍以上的,这也是list引入泛型的一个好处吧,解决了部分拆装和装箱的性能消耗。
那么拆箱和装箱怎么样去避免或者说优化呢。
第一个是避免隐式转换:

     Stopwatch sw = new Stopwatch();
            sw.Start();
            for(int i = 0; i < 80000000; i++)
            {
                // str1 = "joye" + "25";
                 str1 = "joye" +25.ToString();
               // str1 = "joye" + 25;
            }
            sw.Stop();
            Console.WriteLine(sw.Elapsed);

在上面的代码中,其实值类型25是可以隐式转换为string的,那么我测试结果把自己都吓到了,直接隐式转换,23秒,第一种0.4秒,而tostring也只需用19秒左右,这里就要说了,23秒就是频繁的装箱带来的后果,而tostring并未发生装箱,他是值类型int重载了string的tostring编译器在执行时候,会直接去调用这个方法,就并不会去执行装箱操作,同样这也说明了一个,方法重载时候,并不会发生装箱操作。
第二个就是之前测试的利用泛型确定类型
这一点大家有兴趣可以看list引入泛型后带来的优点。
最后几种情况不属于装箱。
1.类型转换,比如父类和子类对象之间的转换,这个并不算是装箱,因为仍然是引用类型。
2.方法重载,如果具有该类型的重载,那么就不发生拆箱或装箱。
比如:int n=10,console.writeline(n);因为writeline这个方法是有int类型的重载的,这和之前的tostring的效果是一样的。
注意:值类型和接口类型的转换也是算装箱和拆箱。
想必装箱和拆箱的性能消耗在上面的测试都是很明显,实际写代码中就应该处理好这个,因为我们经常处理数据。但是这个东西其实很容易搞混,优化时候,就需要用到,所以我在此也记录一下,自己的心得,以作以后用到的时候查看。
鉴于本人水平,如果有不对的地方,还请各位大佬在评论区留下更正,万分感谢。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风萧水寒人往前

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值