漂亮的盒子-装箱拆箱

漂亮的盒子。它里面有什么呢?

Eric Gunnerson
Microsoft Corporation

2001 年 2 月 15 日

上个月我们花了一些时间了解如何找到有关 C# 的内容。我已经收到了一些要求涉及有关 C# 或 .NET 的 Web 站点的请求,因此我决定在专栏中设立一个 Web 站点百宝箱部分。请将您的站点发送给我,每个月我将随机选择五个站点,在专栏的结尾列出它们的 URL。

C# 中的类型

C# 和公共语言运行库 (CLR) 中有两种类型:引用类型(在 C# 中使用类声明)和值类型(在 C# 中使用结构声明)。引用类型和值类型在某些方面有重大的区别。下面是一个汇总了两者差异的表:

 

引用类型(类)

值类型(结构)

变量保存

引用

实际值

值所在位置

位于堆上

内联(位于堆栈上或与对象内联)

默认值

清零的

= 表示

复制引用

复制值

值类型是“感觉上像”一段数据的类型。它包括预定义的数值类型以及用户定义的类型,如 Complex 数、PointRectangle。如上所述,值类型的变量是实际值,所以当使用此变量时,您总是在处理该实际值。

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 自动化变量”类型。

为达到此目的,公共语言运行库提供了一种通过通过一个称为装箱的过程使值类型根据需要变为引用类型的方法。已装箱的类型然后就可以被作为通用基类型的类型对象的变量所引用。

装箱和取消装箱

请考虑下列代码:

int value = 123;

object o = value;   // box int into an object box

int value2 = (int) o;   // unbox into value2

当向 o 赋值时,作为赋值的一部分,C# 编译器将创建足以大到容纳堆上的 int 的引用类型的箱,将值复制到该箱,然后用实际类型(在此情况下为 System.Int32)标记该箱,这样运行库就能够知道该箱中是什么类型。

若要获取箱中的值,必须通过强制转换来指定我们认为箱内存在的类型(这是因为对象可以保存任何类型)。在执行过程中,运行库将检查对象变量引用的类型是否是强制转换中指定的类型。如果类型正确,则将值从该箱复制回值类型变量。如果类型不正确,则引发异常。

请注意,没有任何其他转换可以作为取消装箱操作的一部分发生;类型必须完全匹配。换句话说,如果我们编写:

long value2 = (long) o;   // boxed value is an int

o 是已装箱的 int,则将引发异常。然而,我们可以编写:

long value2 = (long)(int) o;

则转换将按预期进行。

虽然该示例确实说明了装箱和取消装箱,但它仍然有一点儿会引起误解的地方。编写代码以进行装箱的情况相当少见的;它通常在您将值类型变量传递到类型对象的参数时发生。

现在我们要尽心过一个突击测验。我希望你们都专心听讲了。

测验:您对装箱了解了多少?

下面的代码节提供了几个不同的方案。仔细查看它们并判断哪些涉及到了装箱,哪些没有涉及到。某些方案有多个需要检查的地方(例如 B)。

// Scenario 1

int total = 35;

DateTime date = DateTime.Now;

string s = String.Format("Your total was {0} on {1}", total, date);

 

// Scenario B

Hashtable t = new Hashtable();

t.Add(0, "zero");

t.Add(1, "one");

 

// Scenario c

DateTime d = DateTime.Now;

String s = d.ToString();

 

// Scenario IV

int[] a = new int[2];

a[0] = 33;

 

// Scenario 101

ArrayList a = new ArrayList();

a.Add(33);

 

// Scenario vi

MyStruct s = new MyStruct(15);

IProcess ip = (IProcess) s;

ip.Process();

答案

通读答案并查看得分。

方案 1

String.Format() 采用字符串作为第一个参数,使用对象作为第二个和第三个参数。intDateTime 都是值类型,因此它们都将被装箱以成为第二和第三个参数。String.Format() 将采用这些参数,然后对它们分别调用 object.ToString() 以将它们转换为字符串表示形式。知道 int 将被装箱得一分,知道 DateTime 被装箱得一分。

方案 B

Hashtable.Add() 采用两个参数,一个用于键,一个用于值。两者都属于类型对象。为键传递的值是整数,因此必须将其装箱以便作为对象进行传递。为值传递的值是字符串,它是引用类型,因此不对字符串进行装箱。每答对一个得一分。

方案 c

这是一个难懂的方案。其中一个使得能够装箱的原因会导致:可以对值类型调用虚函数,而 ToString() 是对象上的虚函数,因此看起来好像在调用 ToString() 时 d 将被装箱。然而,我们没有在 d 将被转换为对象的情况下使用它,所以不需要进行装箱。编译器知道 DateTime 类型的变量只能是该类型(它不能是导出类型,原因是没有导出的值类型),因此它可以直接调用 DateTime.ToString() 并将“this”引用设置为指向堆栈上的 d 实例。做对得一分。

方案 IV

CLR 中的数组直接存储它们的值。例如,具有五个元素的 int 数组分配足够的空间以存储五个 int,而不是五个对象。如果您回答说这里不进行装箱则得一分。

方案 101

ArrayList.Add() 采用对象作为参数,所以整数 33 将被装箱。做对得一分。

方案 vi

接口是引用类型,所以当您将值类型强制转换为它实现的接口时,该值类型必须被装箱。如果您知道这一点得一分。

给自己打分

把您的得分加起来,并使用下表看您对装箱了解多少:

分数

说明

8

很了解装箱

6-7

了解,但有不清楚的地方

3-5

渴望了解

1-2

找一个有经验的老师吧

0

请继续吃盒饭吧

摘要

现在结束我们对装箱的介绍。装箱使得创作和使用采用一般对象参数的函数变得简单易懂。由于具有如此丰富的内容,因此装箱确实需要一些系统开销。下个月,我们将讨论什么是系统开销、某些减少系统开销的方法和将来对 C# 的扩展——这将使我们在这个领域做得更好。

 

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值