C#代码中背后进行的值拷贝

一种经常发生的装箱

Int32 i = 100;
Console.WriteLine("The number is: " + i);

        通过VS SDK Tools里的IL DASM工具看看产生的IL代码:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       27 (0x1b)
  .maxstack  2
  .locals init ([0] int32 i)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   100
  IL_0003:  stloc.0
  IL_0004:  ldstr      "The number is: "
  IL_0009:  ldloc.0
  IL_000a:  box        [mscorlib]System.Int32
  IL_000f:  call       string [mscorlib]System.String::Concat(object,
                                                              object)
  IL_0014:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0019:  nop
  IL_001a:  ret
} // end of method Program::Main

        可以发现在IL_000a行有一个box装箱操作. 这主要是因为Console.WriteLine方法是输出一个字符串, 这时我们输入了带+号的计算式, 会调用String.Concat(Object arg0, Object arg1)的方法, 如此以来刚刚的Int32数据会被装箱成一个Object数据.

完成一次装箱的步骤

1. 新分配托管堆内存(大小为值类型实例大小加上一个方法表指针和一个SyncBlockIndex)

2. 将值类型的实例字段拷贝到新分配的内存中

3. 返回托管堆中新分配对象的引用地址

避免这样的装箱

        装箱就像给一件物品打包, 这需要一点时间, 上面的代码装箱时间可以忽略不计, 但如果这样的代码出现在一个循环次数比较多的中就需要改进一下. 但避免这样的装箱很简单, 把上面两行代码改成这样:

Int32 i = 100;
Console.WriteLine("The number is: " + i.ToString());

        代码只是简单的将Int32变成一个String类型(引用类型), 有人怀疑ToString()方法会执行一次装箱, 因为他们觉得i是一个值类型, 而String是一个引用类型. 但可以查看这两句产生的IL代码看看有没有发生装箱:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       28 (0x1c)
  .maxstack  2
  .locals init ([0] int32 i)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   100
  IL_0003:  stloc.0
  IL_0004:  ldstr      "The number is: "
  IL_0009:  ldloca.s   i
  IL_000b:  call       instance string [mscorlib]System.Int32::ToString()
  IL_0010:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_0015:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_001a:  nop
  IL_001b:  ret
} // end of method Program::Main

        可以发现ToString()方法并不会产生任何box装箱操作的, 仅仅是值类型获得获得值的字符串表现形式罢了.

值类型与引用类型之间的转换

        在使用new关键字创建一个引用类型对象的时候, 这个对象总是存在在托管堆里, 返回的是指向这个对象的指针. 每一次创建引用类型的实例, 都需要从托管堆中分配内存, 垃圾回收机制会管理着这些内存. 如果每种类型都被这样管理着, 这种机制会对程序的性能产生一些负面影响, 因此对于那些经常使用的简单类型, CLR把他们归于值类型, 它们被分配在堆栈上.

        所有被称为”类”的都是引用类型! 特别注意的是System.String, 它也是个类, 它也是引用类型, 由于一种”字符串驻留”技术, 使它成为了”拥有值类型特点的引用类型”. 而结构或者枚举类型都是值类型, 比如Int32它也只不过是一个struct罢了.

        值类型因为不受垃圾回收机制等等作用, 在某些情况下可以获得更好的性能. 但如果值类型的实例如果经常被某Class经常调用比如被放到List<T>之类的集合(也是类)中, 程序会开辟另外的内存, 把该值类型实例的值拷贝到该内存里…这样做会影响到性能.

        因此我个人觉得值在下面两个情况下拷贝了, 并且我们本不太希望这样的事情发生:

1. 方法传递的参数类型是Object类型. 当然这样的做法是为了能够兼容其它各种类型的参数, 不过通过可以重载这样的方法避免一次值类型->Object类型的操作.

2. 值类型数据被某个Class使用了.

内存何时被释放

        值类型的变量在作用域结束后就自动释放了, 而引用类型都需要通过垃圾回收机制来释放内存.

        但是, Stream也是一个类, 按道理它产生的实例也受托管代码管理, 并有垃圾回收机制对它的资源(内存)进行回收. 但我们还需要输入一遍xxStrean.Close()和xxStream.Dispose(), 原因是内存回收的回收具有不确定性. 如果不写xxStream.Dispose(), CLR的确在某个时刻也会回收它的资源, 只不过出于以下两点考虑, 我们需要输入xxStream.Dispose():

1. 针对Stream类, 内存资源比较有限, 需要及时得释放已经确定不需要再使用的资源. 其他的比如网络连接的资源同样如此.

2. Stream打开的资源大多是独享的, 在它没被释放之前, 如果其它的代码试图再次打开这个资源, 会抛出异常

        当然如果觉得写xxStrean.Close()和xxStream.Dispose()比较烦的话, C#提供了using语句块的用法:

using(FileStream fs = new FileStream(......))
{
    //......
}

        上面代码中的fs会在using语句块结束前得到及时的释放. 当然using后面()中的对象需要实现IDisposable 接口, 这个接口里面提供了Dispose()方法.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值