我知道黑白值类型和引用类型的区别。但为什么我们两者都需要?我们可以使用引用类型而不是值类型。
5 回答
实际上,您可以构建一种只知道引用类型的语言。值类型的存在主要是出于效率原因。例如,将数值类型和 bool 类型实现为值类型会更有效。
要了解值类型,首先必须了解存储位置和引用类型的概念。
对象一般使用字段来表示其状态,运行代码一般使用变量和参数来表示其状态。这些东西,以及阵列槽,统称为“存储位置”。
引用类型的存储位置保存一个对象标识符,或者一个特殊的“空”值。这样的存储位置实际上并不保存对象——只是可以用来快速定位它们的标识符。声明为接口类型(例如ICollection<T>
)的存储位置保存对已知实现指定接口(或“null”)的对象的引用。
复制引用类型的存储位置只是复制标识符。它对引用对象的物理状态没有任何影响。从概念上讲,这可能看起来很简单,但可能很棘手。问题在于,虽然引用类型的存储位置在物理上保存了一个对象标识符,但从语义上讲,这样的存储位置可用于捕获对象的以下任何方面:
- 它的身份(特别是当它与持有对同一对象的引用的其他存储位置相关时)
- 其不变的特性
- 其可变特性
当然,在物理上,引用类型的存储位置保存对象的标识。尽管如此,这些位置通常在语义上用于保存其他信息。例如,考虑一个对象George
包含一个thing
类型为 的字段ICollection<integer>
。这样的存储位置将保存对整数集合的引用,这将允许枚举它们,并且可能允许也可能不允许添加或删除事物。George
的内容如何影响的状态thing
?它可能以多种方式影响国家。
- `George` 可能期望 `thing` 拥有某种不可变集合的身份——它总是会返回相同的一组数字。如果碰巧存在多个包含相同数字的不可变集合,则 `George` 的状态不会受到哪些集合被 `thing` 标识的影响。它的状态仅仅取决于不可变的内容。
- `George` 可能在 `thing` 中存储了对它创建的可变列表的引用,并且它不与任何其他对象共享。`George` 期望 `thing` 引用的列表将保存它放在那里的任何东西,而不是其他任何东西。在这种情况下,`George` 不会关心它的集合是否被另一个具有相同可变特征的集合替换。它只是对该集合的可变内容感兴趣。
- 有可能 `George` 不关心 `thing` 引用的集合的内容,但它的工作是为该集合添加东西,以使其他关心内容的对象受益。在这种情况下,集合的身份是最重要的——'thing' 必须持有对与期望看到 'George' 添加的东西的对象相同的集合的引用。
- 最后,George 可能对可变内容和 `thing` 引用的对象的身份都感兴趣。如果 `George` 是期望其集合从其他对象接收项目的对象,则这种情况将适用。
请注意,在上述所有四种情况下,存储位置的类型完全相同。没有关于该存储位置的声明表明它的哪些方面很重要。进一步注意,许多复杂性源于类引用通常会封装可变状态和身份这一事实。如果存储位置没有封装身份的概念,事情会简单得多。
输入值类型。如果一个存储位置的类型是值类型结构,例如:
结构点3d { 公共双 X,Y,Z; Point3d(双 X,双 Y,双 Z) { 这个.X = X; 这个.Y = Y; 这个.Z = Z; } }
该存储位置将保存该结构的字段的内容(在这种情况下,为fields 、和double
中的三个值。如果一个类具有该类型的字段,则该字段表示的状态将是其中保存的三个数字。不像类对象,等价可能取决于内容,也可能不取决于内容,结构类型没有这种歧义。两个暴露字段结构(有时称为 PODS--Plain Old Data Structures)如果它们是相同的类型,并且如果它们对应字段是等价的。X
Y
Z
尽管类可以做许多值类型不能做的事情,但这种能力会造成混乱、歧义和复杂性。例如,如果想对一个类的状态进行“快照”,就必须知道它的字段的哪些方面对其状态很重要。如果类对象的状态Larry
依赖于Mike
它所引用的对象的可变状态,那么拍摄Larry
' 状态的快照也需要拍摄Mike
' 状态的快照(并存储对该快照的引用在副本的所有字段中,Larry
其中原件Larry
将包含对Mike
) 的引用。相比之下,复制暴露字段结构很容易。只需复制其所有字段。
暴露字段结构是出色的数据持有者,但有几个限制:
- 一个结构(无论是公开字段还是私有字段)可以保存可变数量的数据的唯一方法是保存对保存数据且永远不会更改的对象的引用。由于结构无法知道其某个字段是否包含对对象的唯一现存引用,因此它无法知道是否有任何引用被期望它们不会改变的事物所持有。
- 虽然结构可以在自己的方法中修改自身,但编译器有时会在结构的副本而不是原始结构上执行结构方法。虽然这通常被认为是避免可变结构的原因,但这样的问题只影响会修改自身的结构。它不会影响将其字段暴露给直接外部修改的结构。
假设一个类的状态取决于 5,000 个 3-d 点。如果该Point3d
类型将成为一个类,则必须使该Point3d
类型不可变,或者保留对它的 5,000 个实例的唯一现存引用。如果Point3d
类型是不可变的,那么任何时候想要改变由此指示的状态的任何方面,都必须创建一个全新的Point3d
实例并放弃旧的实例。如果类型是可变的,那么任何时候想要将一个点的坐标暴露给外部代码,都必须复制X
、Y
和Z
值。这两种选择似乎都不是很令人愉快。
制作Point3d
暴露字段结构可以消除这两个问题。由于 aPoint3d
只不过是X
,Y
和Z
, 一个包含 5,000 个对象的数组Point3d
将只包含 15,000 个数字。将 any 暴露Point3d
给外界将自动复制三个关联的数字。如果一个人希望在不影响其他人的情况下改变Z
a 的坐标Point
,那没问题 - 如果Point
存储在一个数组中,可以通过以下方式将 9.8 添加到数组插槽 4 的 Z 坐标
MyArray[4].Z += 9.8;
如果Point
存储在其他类型的集合中,事情会有点尴尬,但还不错:
Point3d temp = MyArray[4]; 温度.Z += 9.8; MyArray[4] = 温度;
比Point3d
上课方便多了。
我认为我能想到的最简单的解释方法是:
值类型表示不变的值。无论是哪个对象,它都1
将永远存在1
,并且3.14159265359
永远存在3.14159265359
。并8/18/2012 17:23 UTC
代表一个始终相同的确切时刻。因此,我可以使用这些值创建一千个不同的int
对象DateTime
,并且无论它们在哪里以及如何使用它们,它们总是完全相同的。
但是,即使规格相同,引用类型也不总是相同的。我可以在给定的地址建造一所房子,然后把计划交给一个朋友,他可以建造另一栋完全相同规格、同样大小、甚至同样景观和同样大小院子的房子,但不管你多么努力地建造它们看起来一样,两栋房子仍然永远不会完全一样。
好吧,也许您错过的差异之一是值类型在堆栈中分配,而引用类型在堆中分配。这会产生很大的性能差异(因为一个是间接访问的指针,另一个是值本身)。
编辑: 我提到堆和堆栈是错误的,如评论中所示。正确的说法应该是 Eric Lippert在这里所说的:
“在桌面 CLR 上的 C# 的 Microsoft 实现中,当值是局部变量或临时值,不是 lambda 或匿名方法的封闭局部变量,并且方法体不是时,值类型存储在堆栈中一个迭代器块,并且抖动选择不注册该值。”
值类型value
直接存储。
例如:
//I and J are both of type int
I = 20;
J = I;
这里,int
是一个值类型,这意味着上面的语句将导致内存中的两个位置。对于值类型的每个实例,都会分配单独的内存并将其存储在堆栈中。它提供快速访问,因为值位于堆栈上。
引用类型存储reference
到值。
例如:
Vector X, Y; //Object is defined. (No memory is allocated.)
X = new Vector(); //Memory is allocated to Object. //(new is responsible for allocating memory.)
X.value = 30; //Initialising value field in a vector class.
Y = X; //Both X and Y points to same memory location. //No memory is created for Y.
Console.writeline(Y.value); //displays 30, as both points to same memory
Y.value = 50;
Console.writeline(X.value); //displays 50.
注意:如果变量是引用的,则可以通过将其值设置为 null 来指示它不引用任何对象。
引用类型存储在堆上,它提供相对较慢的访问,因为值位于堆上。