C# 值与引用类型的误区

在之前的文章**C#类与结构的区别**当中对于结构的说明存在着误区,在拜读了“深入理解C#”这本书的时候,在2.3.3节的说明当中明确指出了三种误区:

  1. “结构是轻量级的类”。
  2. “引用类型保存在堆上,值类型保存在栈上”。
  3. “对象在C#中默认是通过引用传递的”。

这三种经常是我们在平时工作和学习中是这样理解和认为的,在该书当中,作者对于第一个观点就已经提出了一个很好的反例,即 DateTime类型

一个对象的定义是应该使用值类型或者是引用类型,具体应该参考其语义,而不是取决于该类型简单与否。产生这个误区是因为大多数人们认为值类型不需要垃圾回收与类型标识产生开销,也不需要解引用。但是引用类型在其他的地方也更加出色,例如传递参数,赋值等操作的时候,仅需要复制4/8字节,而不需要复制全部数据。

至于第二个误区更常见,变量的值是在他声明的位置存储的,所以假定在一个类当中有一个int型的实例变量,那么在这个类的任意对象,该变量的值总是和对象的其他数据在一起,也就是存储在堆上,只有局部变量和方法参数是存储在栈上。而且对于C#2以及更高的版本,他们的某些局部变量也不都是存储在栈上的。

例如在函数式编程内的闭包:

public Action<int> TestMethod()
{
    int _val = 20;
    Action<int> _result = x => Console.WriteLine(x * _val);
    return _action;
}

public void Test()
{
    var _result = TestMethod();
    _result(10);
    _result(20);
}

在这里_val是一个局部变量,但是这里返回了一个action委托,只要该委托一直存在,那么会保持对_val的引用,这个时候C#会在底层创建一个匿名内,存放在堆当中,以提供给委托使用,除非委托销毁,那么对_val的引用会一直存在。
这里是一个很明显的闭包手法,闭包的作用就是在函数的作用域内保存数据,防止数据出现在无法控制其内容的地方,避免全局变量的使用。

第三个则是所有人误会的最多的的,举个栗子:

public void RefChangle(StringBuilder builder)
{
    builder = null;
}
StringBuilder _sb = new StringBuilder();
RefChangle(_sb);

如果是按照常规说法,builder是按引用传递的话,那么我们在RefChangle方法内部对builder改变了它的值,所以_sb现在应该是null,然而事实并不是这样的。
在这里builder仅仅是“值传递”的_sb的一个引用地址,我们对这个引用地址的更改并不会影响到调用者的对象。
如果将RefChangle的方法方法签名改为如下则是“引用传递”:

public void RefChangle(ref StringBuilder builder)
{
    builder = null;
}