浅尝EffectiveC#_11

Item 18: Distinguish Between Value Types and Reference Types 认识值类型和引用类型的区别

●这不是C++,你可以把所有类型都定义为值类型,并为它们做一个引用。这也不是Java,所有的类型都是值类型。你在创建每个类型实例时,你必须决定它们以什么样的形式存在。这是一个为了取得正确结果,必须在一开始就要面对的重要决定。Value types or reference types? Structs or classes? When should you use each? This isn’t C++, in which you define all types as value types and can create references to them. This isn’t Java, in which everything is a reference type (unless you are one of the language designers). You must decide how all instances of your type will behave when you create it.

●在C#里,你或者用struct声明一个值类型数据,或者用class声明一个引用类型数据。值类型数据应该比较小,是轻量级的。引用类型是从你的类继承来的。这一节将练习用不同的方法来使用一个数据类型,以便你给掌握值类型数据和引用类型数据之间的区别。Value types should be small, lightweight types. Reference types form your class hierarchy. This section examines different uses for a type so that you understand all the distinctions between value types and reference types.

首先,我们来看个例子

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

如果MyData是值类型,返回值会被赋值并存储到v变量中。然而,如果MyData是一个引用类型,你就会将一个引用导入到内部变量上。

或者,考虑下面的变量

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

现在,v是对原始myData的一个赋值。作为引用类型,两个对象在堆上被创建。你不会因为暴露内部数据而感到麻烦。取而代之的是,你在堆上创建了一个额外的对象。如果v是局部变量,它会很快会被视为垃圾,而且Clone要求你在运行时做类型检测。总而言之,这是低效的。Now, v is a copy of the original myData. As a reference type, two objects are created on the heap. You don’t have the problem of exposing internal data. Instead, you’ve created an extra object on the heap. If v is a local variable, it quickly becomes garbage and Clone forces you to use runtime type checking. All in all, it’s inefficient.

以公共方法或属性暴露出去的数据应该是值类型的。但这并不是说所有从公共成员返回的类型必须是值类型的。对前面的代码段做一个假设,MyData有数据存在,它的责任就是保存这些数据。Types that are used to export data through public methods and properties should be value types. But that’s not to say that every type returned from a public member should be a value type. There was an assumption in the earlier code snippet that MyData stores values. Its responsibility is to store those values.

下面来考虑下面的代码片段

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

myType变量仍然是从Foo3返回的。但是这次,取而代之的是访问返回值的内部数据,通过调用一个定义好了的接口上的方法来访问对象。你正在访问一个MyType对象不是它的具体数据,只是使用它的行为。该行为是IMyInterface展示给我们的,同时,这个接口是可以被其它很多类型所实现的。做为这个例子,MyType应该是一个引用类型,而不是一个值类型。MyType的责任是考虑它周围的行为,而不是它的数据成员。

●这段简单的代码开始告诉你它们的区别:值类型存储数据,引用类型表现行为。现在我们深入的看一下这些类型在内存里是如何存储的,以及在存储模型上表现的性能。That simple code snippet starts to show you the distinction: Value types store values, and reference types define behavior. Now look a little deeper at how those types are stored in memory and the performance considerations related to the storage models. Consider this class:
考虑下面这个类:

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

多少个对象被创建了?它们占用多少内存?这还不好说。如果MyType是值类型,那么你只做了一次堆内存分配。大小正好是MyType大小的2倍。然而,如果MyType是引用类型,那么你就做了三次堆内存分配:一次是为C对象,占8字节,另2次是为包含在C对象内的MyType对象分配堆内存。之所以有这样不同的结果是因为值类型是以内联的方式存在于一个对象内,相反,引用类型就不是。每一个引用类型只保留一个引用指针,而数据存储还须要另外的空间。
为了理解这一点,考虑下面这个内存分配:

MyType [] var = new MyType[ 100 ];

如果MyType是一个值类型数据,一次就分配出100个MyType的空间。然而,如果MyType是引用类型,就只有一次内存分配。每一个数据元素都是null。当你初始化数组里的每一个元素时,你要上演101次分配工作--并且这101次内存分配比1次分配占用更多的时间。分配大量的引用类型数据会使堆内存出现碎片,从而降低程序性能。如果你创建的类型意图存储数据的值,那么值类型是你要选择的。

●采用值类型还是引用类型是一个很重要的决定。把一个值类型数据转变为类是一个深层次的改变。考虑下面这种情况:

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 Employee
{
// Properties elided
public string Position
{
get;
set;
}
public decimal CurrentPayAmount
{
get;
set;
}
public void Pay(BankAccount b)
{
b.Balance
+= CurrentPayAmount;
}
}


这扰乱了很多已经存在并使用了你设计的结构的代码。返回值类型的变为返回引用类型。参数也由原来的值传递变为现在的引用传递。下面代码段的行为将受到重创:


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);

就是这个一次性的在工资中添加红利的操作,成了持续的提升。曾经是值类型COPY的地方,如今都变成了引用类型的引用。编译器很乐意为你做这样的改变,你的CEO更是乐意这样的改变。另一方面,你的CFO将会给你报告BUG。你还是没能改变对值类型和引用类型的看法,以至于你犯下这样的错误还不知道:它改变了行为!

出现这个问题的原因就是因为Employee已经不再遵守值类型数据的的原则。另外,定义为Empolyee的保存数据的元素,在这个例子里你必须为它添加一个职责:为雇员付工资。职责是属于类范围内的事。类可以被定义多态的,从而很容易的实现一些常见的职责;而结构则不充许,它应该仅限于保存数据。

●在值类型和引用类型间做选择时,.Net的说明文档建议你把类型的大小做为一个决定因素来考虑。而实际上,更多的因素是类型的使用。简单的结构或单纯的数据载体是值类型数据优秀的候选对象。事实表明,值类型数据在内存管理上有很好的性能:它们很少会有堆内存碎片,很少会有垃圾产生,并且很少间接访问。更重要是:当从一个方法或者属性上返回时,值类型是COPY的数据。这不会有因为暴露内部结构而存在的危险。But you pay in terms of features. 值类型在面向对象技术上的支持是有限的。你应该把所有的值类型当成是封闭的。你可以建立个实现了接口的值类型,但这须要装箱,原则17会给你解释这会带来性能方面的损失。把值类型就当成是一个数据的容器吧,不再感觉是OO里的对象。

●你创建的引用类型可能比值类型要多。如果你对下面所有问题回答YES,你应该创建值类型数据。把下面的问题与前面的Employee例子做对比:

1、类型的最基本的职责是存储数据吗?Is this type’s principal responsibility data storage?
2、它的属性上有定义完整的公共接口来访问或者修改数据成员吗?Is its public interface defined entirely by properties that access its data members?
3、确定这个类型决不会有子类吗?Am I confident that this type will never have subclasses?
4、确定这个类型决不会继续变化吗?Am I confident that this type will never be treated polymorphically?

把值类型当成一个低层次的数据存储类型,把应用程序的行为用引用类型来表现。你会在从类暴露的方法那取得安全数据的COPY。你会从使用内联的值类型那里得到内存使用高率的好处。并且你可以用标准的面向对象技术创建应用程序逻辑。当你对期望的使用拿不准时,使用引用类型。

原文地址:https://www.cnblogs.com/TivonStone/p/1759288.html