包装很好,里面是什么?
作者:Eric Gunnerson
上个月,我们花了一些时间来学习如何找到与 C# 有关的东西。我收到一些询问,问及有关 C# 或 .NET 的 Web 站点,所以我决定在专栏里增加一个 Web 站点荟萃节目。写信告诉我您的站点,每个月我会随机选择五个站点,在专栏的最后列出它们的 URL。
开场白结束,言归正传。
C# 中的类型
C# 和公共语言运行时 (CLR) 中有两种类型:引用类型(在 C# 中用类声明)和值类型(在 C# 中用结构声明)。引用和值类型在几个重要方面有所不同。下表概括了这些区别:
引用(类)
值(结构)
保留变量
引用
实际值
活动值
在堆中
内联(在堆栈中或与对象内联)
默认值
Null
零
= 表示
复制引用
复制值
值类型“感觉上”象一个数据。它包括预定义数值类型以及用户定义的类型(如 Complex 数字、Point 或 Rectangle)。如上文所述,值类型的变量是实际的值,所以在您使用变量时,通常处理的是实际的值。
int i = 123;
int j = i;
i = 55;
在第二次指定变量后,两个独立的变量包含相同的值。修改 i 的值不会改变 j 的值。
引用类型用于所有不能用作值类型的对象。引用类型的变量指向堆中对象的实例。这意味着在将一个变量指定给另一个变量时,只是指定了引用,而不是值。
Employee e = new Employee("Fred");
Employee f = e;
f.Name = "Barney";
在第二次指定变量后,e 和 f 指向同一对象。这意味着修改 f 的名称也将改变 e 的名称,因为它们引用同一实例。
这引发了一个相关话题。有些人可能一直奇怪,为什么 System.String 类中的函数不修改字符串,而总是返回字符串的新副本。这是因为字符串的类型为引用类型。如果对字符串调用 s.Trim() 来修改内部字符串,您将遇到与 Employee 相同的问题(这对字符串非常糟糕)。
修改类值的成员称为“变更者”,而不具有任何变更者的类称为不可变类。不可变类的存在可以使类的行为类似于值类,但不能写入为值类。
如果您需要使用可变字符串类,请在 System.Text 中试用 StringBuilder。
转向更简单的模型
在语言中同时使用引用和值两种类型是很重要的。值类型轻便高效,而引用类型适用于面向对象的开发。但是,现在我们有两种类型,而我们需要的是更为简单的模型,使用单一的、能够囊括所有可能值的类型。
这样一个通用基类能够:
调用任何值的虚函数。
写入能够存储任何值的集合类。
替代 OLE Automation Variant 类型。
为实现这一目的,公共语言运行时采用一种方法让值类型在需要时转化为引用类型,即通过称为包装的进程。被包装的类型是通用基类,可以被各种类型的对象引用。
包装和解除包装
考虑下列代码:
int value = 123;
object o = value; // 将 int 包装到对象中
int value2 = (int) o; // 解除包装到 value2
当赋值给 o 时,作为赋值的一部分,C# 编译器将创建足够容纳堆中 int 的引用类型包装,将值复制到该包装,然后将包装标记为实际类型(此处为 System.Int32),以便运行时了解包装的类型。
要从包装中取值,必须使用强制类型装换来指定包装的类型(对象能够保留任何类型)。在执行过程中,运行时将检查对象变量引用的类型是否为强制类型转换中指定的类型。如果类型正确,值将从包装中复制回值类型变量。如果类型不正确,将导致异常。
请注意解除包装过程中不会进行其他转换;类型必须完全匹配。换句话说,如果我们编写代码:
long value2 = (long) o; // 包装的值是 int
其中 o 为已包装的 int,则将导致异常。但是,我们可以这样编写:
long value2 = (long)(int) o;
则转换将正常进行。
虽然这个示例演示了包装和解除包装,但可能会造成一些误解。编写代码进行包装的情况非常少见,一般是在将值类型的变量传递给类型对象参数时使用。
下面是一个小测验。希望大家用心做。
测验:您对包装了解多少?
下面的代码段给出了不同方案。请阅读这些代码,判断哪一段代码涉及包装,而哪一段与包装无关。有些方案(如 B)需要检查多个位置。
// 方案 1
int total = 35;
DateTime date = DateTime.Now;
string s = String.Format("Your total was {0} on {1}", total, date);
// 方案 B
Hashtable t = new Hashtable();
t.Add(0, "zero");
t.Add(1, "one");
// 方案 c
DateTime d = DateTime.Now;
String s = d.ToString();
// 方案 IV
int[] a = new int[2];
a[0] = 33;
// 方案 101
ArrayList a = new ArrayList();
a.Add(33);
// 方案 vi
MyStruct s = new MyStruct(15);
IProcess ip = (IProcess) s;
ip.Process();
答案
请检查答案并记录分数。
方案 1
String.Format() 将字符串作为第一个参数,对象作为第二个和第三个参数。int 和 DateTime 都是值类型,所以它们都将被包装以作为第二个和第三个参数。String.Format() 使用这些参数,然后对每个参数调用 object.ToString() 将其转换为字符串表示。如果您知道 int 将被包装,得一分;如果知道 DateTime 被包装,得一分。
方案 B
Hashtable.Add() 有两个参数,一个是关键字,另一个是值。它们都是类型对象。传递给关键字参数的值为整数,所以它必须被包装,才能作为对象传递。传递给值参数的值为字符串(引用类型),所以字符串不需要包装。每一点判断正确各得一分。
方案 c
这段代码很迷惑人。包装的目的之一是实现对值类型参数的虚函数调用。ToString() 是对象的虚函数,所以,看起来在调用 ToString() 时,d 将被包装。但是在转换对象时没有使用 d,所以不需要进行包装。编译器知道类型为 DateTime 的变量只能为该类型(因为没有导出的值类型,所以该变量不能为导出类型),所以它可以直接调用 DateTime.ToString(),并设置“这个”引用,使其指向堆栈中的 d。如果回答正确,得一分。
方案 IV
CLR 中的数组直接保存它们的值。例如,一个有五个元素的 int 数组有足够的空间保存 5 个 int,而不是 5 个对象。如果认为此代码与包装无关,得一分。
方案 101
ArrayList.Add() 将对象作为参数,所以整数 33 将被包装。如果回答正确,得一分。
方案 vi
接口为引用类型,所以在将值类型强制转换到接口实现时,必须包装值类型。如果了解这一点,得一分。
最后得分
将所有分数相加,并使用下表来检查对包装的了解程度:
分数
说明
8
对包装很了解
6-7
了解,但有时明白,有时糊涂
3-5
尚可,请再接再厉
1-2
需要继续学习
0
尚未入门
摘要
最后总结一下包装。包装使编写和使用具有通用对象参数的函数变得简单而直接。和许多美好的事情一样,包装也有不好的一面。下个月,我们将讨论这些不好的方面是什么,如何减少负面影响,以及对 C# 的进一步说明(这会让我们的 C# 生活更加美好)。
C# Web 站点荟萃
下面的 Web 站点是我通过电子邮件获得的,不过我知道的仅此而已。如果那里的代码无法编译,计算机死机,或者遇到种种麻烦,请不要向我抱怨。
本月仅有两项:
Eric Gunnerson 是 C# 编译器组的 QA 领导,C# 设计组的成员,以及“A Programmer's Introduction to C#”的作者。他做编程的时间很长,甚至知道什么是 8 英寸磁盘,还能用一只手装磁带。
已存档的 Working With C# 专栏文章