Da Vinci's CyberSpace

手把青秧插满田, 低头便见水中天; 心地清净方为道, 退步原来是向前.
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

.NET下午茶之二:值类型与引用类型的区别(转载)

Posted on 2008-03-20 08:37  Da Vinci  阅读(1218)  评论(3编辑  收藏  举报

使用值类型还是引用类型?结构体(structs)还是类(class)?什么情况下两者都可以使用?这并不是C++,你可以为任何类型的对象建立 指针来引用他们。这也不是java,任何类型都自动声明为引用类型。你需要想清楚你将要定义的类型会有怎样的行为。首次能否选择对是至关重要的,一旦你决 定使用哪种类型,你就要承担相应的后果,因为如果你后面修改了你之前定义的类型将会给你的代码带来潜在的不连贯性。使用struct或class关键字来 创建你的类型是件简单的事情,但是在后面更新或修改这些你所定义的类型将需要做更多的工作。

选择正确的类型而不是你所偏好的类型并不是件简单的事情。正确的选择取决于你期望如何使用你所定义的新类型。值类型不支持多态。它更倾向 于用在为你的应用程序来存储数据上。引用类型支持多态而且应该用在定义你程序的行为上。考虑以上你所定义的新类型将会用于哪种用途,然后根据正确的用途来 决定你将要创建的类型。结构体(struct)用于存储数据。类(class)用于定义行为(beahavior)

值类型和引用类型之间的区别被加进.Net和C#中是因为在C++和Java中这些问题很普遍。在C++中,所有的参数和返回类型都是以 值类型的方式传递的。通过值类型来传递具有不错的效率,但它却有一个问题:部分拷贝(partial copying)也有人称作对象的切片(slicing the object)。你期望的是使用一个派生的对象,然而只有基类的部分被拷贝了。也就是你丢失了所有派生类对象中所保存的信息。甚至你对虚函数的调用也是基 类的版本。

Java语言的对策是从语言中或多或少的消除值类型。所有用户定义的类型都是引用类型。在Java语言中所有参数和返回类型都是以引用方 式传递的。这种策略具有一致性的优点,但是它却牺牲了效率,因为有些类型根本没有必要是多态的。因此Java程序员需要在堆上分配空间和最终对每个变量进 行垃圾回收。它们同样需要为每个非引用类型而消耗额外的时间。所有的变量都是引用类型的。在C#中,你通过使用struct和class关键字来声明你的 类型是值类型还是引用类型。值类型应是当更小,更轻量的类型。引用类型组成了你的类的层次。这段例子分别使用值类型和引用类型来帮助你理解这两者之间的区 别。

在一开始,这个类型使用一个方法返回的值。

private MyData _myData;
public MyData Foo()
{
 return _myData;
}
 
// call it:
MyData v = Foo();
TotalSum += v.Value;

如果MyData是值类型,它给v返回了一份值的拷贝。更多的,v是在栈上的。然而,如果MyData是一个引用类型,你传递了引用,也就是你把内部数据输出给外部变量。这样会违反封装的原则。

或者我们来考虑做个变化:

private MyData _myData;
public MyData Foo()
{
 return _myData.Clone( ) as MyData;
}
 
// call it:
MyData v = Foo();
TotalSum += v.Value;

现在,v就是_myData原版的一个拷贝。作为一个引用类型,它在堆上创建了两个对象。这样你就不会再有暴露内部数据的问题了。作为替代,你在堆 上建立了额外的对象。如果v是一个局部变量,那么它很快就会变为垃圾碎片而且克隆(clone)会强制进行运行时检查。总的来说,这是一种没有效率可言的 做法。

用于从公共方法或属性来获得数据的类型应当被定义为值类型。但是这并不等同于任何从公共方法返回的类型就是值类型。前面的代码段假设MyData存储了值,那么它的职责就是存储这些值。

但是,考虑下面的代码段。

private MyType _myType;
public IMyInterface Foo()
{
 return _myType as IMyInterface;
}
 
// call it:
IMyInterface iMe = Foo();
iMe.DoWork( );

_myType变量依然是通过Foo方法获得的返回值。但是这次不同的是,这次没有访问返回值的数据,而是访问了定义在接口中的方法。你访问的并不 是MyType的数据内容,而是它的行为。它的行为是通过ImyInterface这个接口所体现的,也就是说它可以表现成其他不同的行为。在这个例子 中,MyType应当是一个引用类型而不是值类型。MyType的职责包含了它的行为,而不是它的数据成员。

上面的代码段向你展示了它们之间的区别:值类型存储数据而引用类型定义行为。现在我们更深入的观察它们在内存中的存储方式以及相关存储模型所带来性能上的问题。考虑这个类:

public class C
{
  private MyType _a = new MyType( );
  private MyType _b = new MyType( );
 
  // Remaining implementation removed.
}
 
C var = new C();

上面的代码里有多少个对象被创建了?它们分别是多大?它视情况而定,如果MyType是一个值类型,你只需要进行一次分配。这次分配的大小就是 MyType大小的两倍。然而,如果MyType是一个引用类型,你需要做3次分配:一次是为c对象来分配空间,大小是8字节(假设指针的大小是 32bit),剩下两次则是为包含在c对象中的MyType对象。结果会有差别是因为值类型是以内联(inline)方式存储在对象内的,而引用类型不 是。每个引用类型的变量只保留一个对象的引用,而实际的存储则是需要额外的空间。

为了彻底的阐明以上观点,现在来考虑一下这种情况的空间分配:

MyType [] var = new MyType[ 100 ];

如果MyType是值类型,一次空间分配将会产生MyType对象大小的一百倍空间。然而,如果MyType是引用类型,则只会发生一次空 间分配。这个数组中的每一个元素都是null。在堆上分配大量的引用类型变量会使堆变得凌乱琐碎而且降低运行速度。如果你只是为了存储数据的话,那么值类 型将会是正确的选择。

对使用值类型还是引用类型所做的讨论是很有意义的,将一个值类型转换成引用类型将会进入更深层次的研究。考虑下面的情况:

public struct Employee
{
  private string  _name;
  private int     _ID;
  private decimal _salary;
 
  // Properties elided
 
  public void Pay( BankAccount b )
  {
    b.Balance += _salary;
  }
}

上面的简单类型包含了一个可以让你支付自己员工的方法。一段时间过去了,系统运行的相当不错,但是此后你开始定义不同级别的员工:推销员得到佣金,而经理得到奖金。你决定把Employee由值类型改为引用类型。

public class Employee
{
  private string  _name;
  private int     _ID;
  private decimal _salary;
 
  // Properties elided
 
  public virtual void Pay( BankAccount b )
  {
    b.Balance += _salary;
  }
}

这个改变破坏了你的用户所使用的大部分代码。返回值变成了返回引用。以前参数传递的是值而现在传递的是引用。下面这一小段的代码行为被彻底的改变了:

Employee e1 = Employees.Find( "CEO" );
e1.Salary += Bonus; // Add one time bonus.
e1.Pay( CEOBankAccount );

每一次加奖金都将是永久的增加。以前通过值拷贝而如今被引用所替代。编译器很乐于为你做这些事情。CEO肯定也很高兴。但另一方面CFO会汇报这个bug。当面对这样的实际情况时你就不能产生使用引用类型来代替值类型的想法了:因为它改变了行为。

之所以会产生这样的问题时因为Employee类型不再遵循使用值类型的原则。你所定义的Employee类型不仅储存了有关员工的数据信 息,而且还定义了它的行为,在这个例子中它具有支付员工的行为。这些职责是属于类(class)的范畴。类可以使得实现公共职能多态化更加简单;而结构体 就无法做到,它应当被限制为只用于数据存储。

.Net的支持文档里推荐根据类型的大小来考虑使用值类型或是引用类型。在实际中,值类型善于用在类型易于构造或用于携带数据的情况下。 在某些方面值类型更加利于内存的管理:产生更少的堆碎片、更少的垃圾。最重要的是当值类型从方法或属性返回时使用的是拷贝。这就避免了暴露内部数据结构的 风险。但是你也将会为这些特点付出一定的代价。值类型在支持面相对象的技术方面有很大的限制,你无法通过值类型来简直具有层次的对象,你只能把所有的值类 型的对象看成封闭(sealed)的来考虑。你可以用值类型来实现接口,但是那需要采用拆装箱技术,这将会带来性能上的损失。考虑值类型只作为存储数据的 容器,而不具有面相对象的意义。

你还是会更加常用到引用类型。如果你能肯定的回答以下的这些问题,那么你就可以放心的使用值类型。比较之前的Employee的例子来考虑下面这些问题:

1. 你所要声明的类型是否只承担存储数据的责任。

2. 你所要声明的类型的数据访问接口是否全部定义为了属性(properties)。

3. 你是否确定它永远都不会有派生类。

4. 你是否确定它永远都不会被看作多态对待。

构建低等级的数据存储类型时使用值类型。构建具有行为的程序时使用引用类型。你从你创建的类的对象中安全获得数据拷贝。以栈作为基础并使用 内联的值存储方式更加有益与内存的利用,你可以利用面相对象的标准技术来建立你引用程序的逻辑。当你疑惑该使用哪种类型时,使用引用类型吧!

参考:http://blog.csdn.net/knight94/archive/2006/07/01/861383.aspx
Firefox 3