原则18:值类型和引用类型的区别

优质
小牛编辑
132浏览
2023-12-01

值类型或引用类型?结构体或类?什么时候你需要使用它们?这不是 C++ ,定义的类型为值类型可以当做引用类型使用。这也不是 Java ,所有类都是引用类型(除非你是语言设计者之一)。当你创建类的时候你就需要决定这个类所有实例的行为。在开始的时候就要做好这个重要的选择。你必须面对这个选择的后果因为改变之前的选择会引起一些代码的破坏。创建类型的时候只是很简单的选择 struct 和 class 关键字,但是如果改变类型就要花很大功夫去更新客户代码。

这不想更喜欢其中的一个那么简单。正确的选择取决于你希望怎么样使用新类型。值类型不会有多态性。它们更适合用于存储程序操作的数据。引用类型具有多态性应该用来定义程序的行为。考虑新类型期待的职责,并且选择正确的类型来创建。结构体存储数据。类定义行为。

.NET 和 C# 会加入值类型和引用类型的区别是因为在 C++ 和 Java 都出现很常见的问题。在 C++ 中,所有参数和返回值都按值传递。按值传递是非常高效的,但是会有一个问题:部分复制(有时也被称作对象切割)。如果你在希望是基类的地方使用的是子类,只有子类中基类中的部分会被复制。你因此失去子类的所有信息。即使是调用虚函数也是执行的基类的版本。

Java 语言的针对这个问题处理是或多或少移除值类型。所有用户定义的类型都是引用类型。在 Java 语言中,所有参数和返回值都是按引用传递的。这个策略有着保持一致的优点,但是牺牲了性能。我们来正视这个问题,有些类不需要多态性——它们也不会被那样设计。Java 程序员为每个变量都需要堆内存的分配和最后的垃圾回收。他们还需要花费更多的时间消耗去解引用每个变量。所有变量都是引用。在 C# 中,你声明新类型是值类型还是引用类型取决于你使用的是 struct 还是 class 关键字。值类型会是小的,轻量的类型。引用类型会出现你类型的继承结构中。这个部分会检查类型的不同使用让你明白值类型和引用类型的所有区别。

首先,这个类型作为方法的返回值:

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

如果 MyData 是值类型,返回值会被复制存储为 v 。然而,如果 MyData 是引用类型,会导出把内部的变量的引用。你就破坏了封装的原则(查看原则26)。

或者,考虑下面的变种:

public MyData Foo2() 
{
    return myData.CreateCopy(); 
}
// call it: 
MyData v = Foo(); 
TotalSum += v.Value;

现在, v 是 myData 的复制。如果是引用类型,在堆内存会创建两个对象。你不会有暴露内部数据的问题。而是,你在堆内存创建另一个对象。如果 v 是一个局部变量,它会很快变为垃圾并且克隆会强制进行类型检查。这些都是低效率的。

如果类使用 public 方法和属性导出数据应该使用值类型。但那不是说所有有 public 的成员的类型都得是值类型。前面的代码就是假设 MyData 是存储值。它的职责就是存储这些值。

但是考虑下面代码:

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

myType 仍然从 Foo3 方法中返回。但是这次,不是从返回值中访问数据,而是访问对象定义接口的方法。你访问 MyType 对象不是它存储的数据而是它的行为。那个行为是通过 IMyInferface 体现的,它可以被多个类实现。对于这里例子, MyType 就需要是引用类型,而不是值类型。 MyType 的职责是围绕它的行为,而不是它的数据。

这段简单的代码开始告诉你的区别:值类型存储值,引用类型定义的行为。现在看得更深一点,在这些类型存储在内存和存储模型上表现的性能。考虑这个类:

public class C 
{
    private MyType a = new MyType(); 
    private MyType b = new MyType();
    // Remaining implementation removed. 
}
C cThing = new C();

多个对象被创建?它们会占多大内存?这还不能下定论。如果 MyType 是值类型,你只有一次内存分配。分配的大小是 MyType 大小的两倍。然而,如果 MyType 是一个引用类型,你就有三次内存分配,第一个是 C 对象,它是一个8字节(指针占32位),另外两个是 C 对象包含的 MyType 对象。这个区别是因为值类型会存储在对象内部,而引用类型不会。每个引用变量持有引用,存储需要额外的内存分配。

为了证明这一点,考虑下面内存分配:

MyType[] arrayOfTypes = new MyType[100];

如果 MyType 是值类型,会一次分配100个的 MyType 对象。然而,如果 MyType 是引用类型,只有一次分配发生。数组的每个元素都是 null 。当你初始化数组100个元素,你就已经有101内存分配——101次多一次内存分配。分配大量的引用类型会将对内存碎片化就会变得慢下来。如果你创建的类型是为了存储数据,那么值类型就是要选择的方式。

值类型和引用类型是一个很重要的选择。将值类型改为引用类型会有影响深远的改变。考虑这个类型:

public struct Employee 
{
    // Properties elided 
    public string Position 
    {
        get; 
        set;
    }
    public decimal CurrentPayAmount 
    {
        get; 
        set;
    }
    public void Pay(BankAccount b) 
    {
        b.Balance += CurrentPayAmount; 
    }
}

这个简单类型只包含一个方法让你支付被雇佣者。时间久了,系统也会运行很好。如果你决定有不同类型的被雇佣者:销售人员得到佣金,经理收到奖金。你决定改变 Employee 的类型:

public class Employee2 
{
    // Properties elided 
    public string Position 
    {
        get; 
        set;
    }
    public decimal CurrentPayAmount 
    {
        get; 
        set;
    }
    public virtual void Pay(BankAccount b) 
    {
        b.Balance += CurrentPayAmount; 
    }
}

这个会破坏很多已经存在使用你定义 struct 的代码。返回值类型变为返回引用类型。参数按值传递变为按引用传递。下面代码的行为会有巨大的变化:

Employee e1 = Employees.Find(e => e.Position == "CEO"); 
BankAccount CEOBankAccount = new BankAccount(); 
decimal Bonus = 10000; 
e1.CurrentPayAmount += Bonus; // Add one time bonus. 
e1.Pay(CEOBankAccount);

一次性的奖金变成了永久的加薪。这就是用引用类型替换值类型导致的。编译器会十分欢快是你的改变生效。 这个 CEO 也会很高兴。另一个头, CFO 就会报错。你不能想当然把值类型改为引用类型因为实际上:它改变行为了。

这个问题出现是因为 Employee 类不再遵循值类型的规范。定义的被雇佣者除了存储数据,在这个例子中,它还有其他职责——支付被雇佣者。职责是类类型的范畴。类可以定义多态很容易实现常见的职责,结构体就不能只应该限制存储数据。

.NET 文档推荐你考虑类型大小作为决定是否值类型和引用类型的决定因素。实际上,更好的因素是类型的使用。如果类型只是一个结构或者数据的载体最佳的候选是值类型。的确值类型在内存管理更高效:少量的堆内存碎片,更少的垃圾,以及更少寻址。更重要的是,值类型作为方法或属性的返回值会被复制。暴露内部结构体的引用没有任何危害。但是注意其他的细节。值类型对支持常见的面向对象技术很有限制。你不能创建值类型的继承结果。你应该认为所有的值类型都是封闭的。你创建的值类型可以实现接口但是需要装箱,原则17介绍拆箱操作引起的性能问题(译者注:这应该是第一步叙述,第一个版中原则17关于封箱和拆箱)。你应该认为值类型作为存储的容器,而不是面向对象意义上的对象。

你会创建比值类型更多的引用类型。如果下面的问题你都回答是,那你可以创建值类型。以前面雇佣者为例考虑这些问题:

1.这个类是否满足存储数据的职责?

2.是否所有属性 public 接口都只是访问数据?

3.我是否确定这个类型不会有子类?

4.我是否确定这个类型不会看成多态性?

构建低级的数据存储类就用值类型。使用引用类型构建你程序的行为。你可以安全的复制数在对象外暴露。你可以享受基于栈和内置值存储的内存使用优势,而且你可以利用标准的面向对象计算实现你程序的逻辑。当对类型选择很疑惑的时候,就使用引用类型。

小结:

总结下:值类型和引用类型的选择——如果只是数据的存储,并且所有 public 的接口(属性和方法)都是只是访问数据而不是修改数据才使用值类型,其他情况都选择引用类型。

欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!