为什么在C#中需要装箱和拆箱?


323

为什么在C#中需要装箱和拆箱?

我知道装箱和拆箱是什么,但我无法理解其实际用途。为什么在哪里使用?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing

Answers:


480

为什么

要拥有统一的类型系统并允许值类型对其基础数据的表示形式与引用类型表示其基础数据的方式(例如, int只是一个32位的存储桶,与引用完全不同)类型)。

这样想吧。您有一个o类型的变量object。现在您有了一个,int并且想要将其放入oo是对某处某物的引用,而int强调不是某处某物的引用(毕竟,它只是一个数字)。因此,您要做的是:创建一个object可以存储的新int对象,然后将对该对象的引用分配给o。我们称此过程为“装箱”。

因此,如果您不关心拥有统一的类型系统(即,引用类型和值类型具有非常不同的表示形式,并且您不希望使用通用的方式“表示”这两者),则无需装箱。如果您不关心int表示其基础值(即也int可以是引用类型,而只是存储对其基础值的引用),则不需要装箱。

我应该在哪里使用它。

例如,旧的集合类型ArrayList只吃objects。也就是说,它仅存储对居住在某处的事物的引用。没有拳击,你不能放一个int这样的收藏中。但是使用拳击,您可以。

现在,在泛型时代,您实际上并不需要它,并且通常可以在不考虑问题的情况下轻松进行。但是需要注意一些注意事项:

这是对的:

double e = 2.718281828459045;
int ee = (int)e;

这不是:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)o; // runtime exception

相反,您必须执行以下操作:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)(double)o;

首先,我们必须明确地取消double(double)o)的装箱,然后将其转换为int

以下结果是什么:

double e = 2.718281828459045;
double d = e;
object o1 = d;
object o2 = e;
Console.WriteLine(d == e);
Console.WriteLine(o1 == o2);

在继续下一句话之前,请先考虑一下。

如果你说的TrueFalse伟大!等一下 那是因为==在引用类型上使用引用相等性,它检查引用是否相等,而不是检查基础值是否相等。这是容易犯的危险。也许更加微妙

double e = 2.718281828459045;
object o1 = e;
object o2 = e;
Console.WriteLine(o1 == o2);

还将打印False

最好说:

Console.WriteLine(o1.Equals(o2));

幸运的是,它将打印出来True

最后一个微妙之处:

[struct|class] Point {
    public int x, y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Point p = new Point(1, 1);
object o = p;
p.x = 2;
Console.WriteLine(((Point)o).x);

输出是什么?这取决于!如果Point是,struct则输出为;1如果Point是,class则输出为2!装箱转换将复制装箱的值,以解释行为上的差异。


@Jason您的意思是说,如果我们有原始列表,则没有理由使用任何装箱/拆箱?
Pacerier,2012年

我不确定“原始列表”是什么意思。
杰森2012年

3
您能否谈谈boxingand 对性能的影响unboxing
凯文·梅瑞迪斯


2
出色的答案-比我在知名书籍中阅读的大多数解释都更好。
FredM

58

在.NET框架中,有两种类型的类型-值类型和引用类型。这在OO语言中相对常见。

面向对象语言的重要特征之一是能够以与类型无关的方式处理实例。这称为多态。由于我们想利用多态性,但是我们有两种不同的类型,因此必须有某种方法将它们组合在一起,以便我们可以用相同的方式处理一个或另一个。

现在,在过去(Microsoft.NET的1.0)中,没有这种新奇的泛型hullabaloo。您无法编写具有单个参数的方法,该方法可以为值类型和引用类型提供服务。这违反了多态性。因此,采用装箱作为将值类型强制为对象的方法。

如果无法做到这一点,则该框架将堆满方法和类,这些方法和类的唯一目的是接受其他类型的类型。不仅如此,而且由于值类型并不真正共享一个共同的类型祖先,因此对于每种值类型(位,字节,int16,int32等),您都必须具有不同的方法重载。

拳击阻止了这种情况的发生。 这就是为什么英国人庆祝节礼日。


1
在使用仿制药之前,自动装箱是完成许多事情所必需的。考虑到泛型的存在,如果不是需要保持与旧代码的兼容性,我认为.net如果没有隐含的装箱转换,会更好。强制转换为的值类型List<string>.EnumeratorIEnumerator<string>产生一个对象,该对象的行为基本上类似于类类型,但Equals方法已损坏。强制转换List<string>.Enumerator为更好的方法IEnumerator<string>是调用自定义转换运算符,但是隐含转换的存在阻止了该转换。
超级猫2012年

42

理解这一点的最好方法是查看C#构建于其上的较低级编程语言。

在最低级的语言(如C)中,所有变量都放在一个位置:堆栈。每次声明变量时,它都会进入堆栈。它们只能是原始值,例如bool,字节,32位int,32位uint等。堆栈既简单又快速。随着变量的添加,它们只是一个接一个地运行,因此,您声明的第一个变量位于内存中,例如0x00,下一个位于0x01,下一个位于0x02,等等。此外,变量通常在compile-时间,因此甚至在运行程序之前就知道其地址。

与C ++一样,在下一个升级中,引入了第二种称为“堆”的内存结构。您仍然主要生活在堆栈中,但是可以将称为指针的特殊整数添加到堆栈中,以存储对象的第一个字节的内存地址,并且该对象位于堆中。堆有点混乱,并且维护起来有点昂贵,因为与堆栈变量不同,它们在程序执行时不会线性地递增然后递减。它们可以没有特定的顺序来来去去,并且可以成长和收缩。

处理指针很难。它们是导致内存泄漏,缓冲区溢出和失败的原因。用C#进行救援。

在更高级别的C#中,您无需考虑指针-.Net框架(用C ++编写)会为您考虑这些指针,并将其作为对对象的引用呈现给您,并且为了提高性能,您可以存储更简单的值如布尔值,字节和整数作为值类型。在幕后,实例化Class的Objects和东西放在昂贵的内存管理堆上,而Value Types和低级C放在同一堆栈中-超快。

为了使这两个根本不同的内存概念(和存储策略)之间的交互保持简单,从编码人员的角度出发,可以随时将“值类型”装箱。装箱会导致从堆栈中复制值,将其放入对象中,然后放置在堆中 -代价更高,但与参考世界的交互作用较不稳定。正如其他答案指出的那样,当您说:

bool b = false; // Cheap, on Stack
object o = b; // Legal, easy to code, but complex - Boxing!
bool b2 = (bool)o; // Unboxing!

关于Boxing优势的一个有力说明是对null的检查:

if (b == null) // Will not compile - bools can't be null
if (o == null) // Will compile and always return false

从技术上讲,我们的对象o是堆栈中的地址,该地址指向我们的bool b的副本,该副本已复制到堆中。我们可以检查o是否为null,因为布尔值已装箱并放在其中。

通常,除非需要,否则应避免使用Boxing,例如将int / bool / whatever作为对象传递给参数。.Net中有一些基本结构仍然需要将值类型作为对象传递(因此需要装箱),但是在大多数情况下,您不需要装箱。

需要装箱的历史C#结构的非详尽列表,应避免:

  • 事实证明,事件系统在其天真的使用中具有“竞争条件”,并且不支持异步。添加拳击问题,应该避免。(例如,您可以将其替换为使用泛型的异步事件系统。)

  • 旧的Threading和Timer模型在其参数上强制使用Box,但已被async / await替代,后者更加简洁高效。

  • .Net 1.1集合完全依赖Boxing,因为它们先于Generics。这些仍然在System.Collections中启动。在任何新代码中,您都应该使用System.Collections.Generic中的Collections ,除了避免Boxing之外,它还为您提供了更强的类型安全性

您应该避免将值类型声明为对象或将其作为对象传递,除非必须处理上述导致装箱的历史问题,并且要避免在以后知道装箱的情况下装箱对性能的影响。

Per Mikael的建议如下:

做这个

using System.Collections.Generic;

var employeeCount = 5;
var list = new List<int>(10);

不是这个

using System.Collections;

Int32 employeeCount = 5;
var list = new ArrayList(10);

更新资料

该答案最初建议Int32,Bool等引起装箱,而实际上它们是值类型的简单别名。也就是说,.Net具有Bool,Int32,String和C#之类的类型,它们将别名化为bool,int,string,而没有任何功能上的区别。


4
您教了我一百年来程序员和IT专业人员无法解释的内容,但将其更改为说该做什么而不是避免什么,因为这很难遵循。基本规则通常不适用1 。您不应该这样做,而是应该这样做
Mikael Puusaari

2
这个答案应该被标记为ANSWER一百次了!
普扬

3
C#中没有“ Int”,有int和Int32。我认为您在陈述一种是值类型而另一种是包装值类型的引用类型时错了。除非我弄错了,否则在Java中是正确的,但在C#中不是。在C#中,在IDE中显示为蓝色的是其结构定义的别名。因此:int = Int32,布尔=布尔值,字符串=字符串。之所以在布尔值上使用布尔值是因为在MSDN设计指南和约定中建议使用布尔值。否则我喜欢这个答案。但是我将投反对票,直到您证明我错了或在回答中解决该问题为止。
Heriberto Lugo

2
如果将一个变量声明为int,另一个变量声明为Int32,或者将bool和Boolean声明为-右键单击并查看定义,则最终将得到结构的相同定义。
Heriberto Lugo

2
@HeribertoLugo是正确的,“您应该避免将值类型声明为Bool而不是bool”这一行是错误的。正如OP指出的那样,您应避免将布尔值(或布尔值或任何其他值类型)声明为Object。bool / Boolean,int / Int32只是C#和.NET之间的别名:docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/…–
STW,

21

装箱实际上并不是您要使用的东西,而是运行时使用的东西,以便您可以在必要时以相同的方式处理引用和值类型。例如,如果使用ArrayList来保存整数列表,则将这些整数装箱以适合ArrayList中的对象类型插槽。

现在使用通用集合,这几乎消失了。如果创建List<int>,则不会进行装箱- List<int>可以直接保存整数。


您仍然需要装箱来进行诸如复合字符串格式之类的操作。使用泛型时,您可能不会经常看到它,但是它肯定仍然存在。
杰里米·S

1
是的-它也始终在ADO.NET中显示-不管实际数据类型是什么,sql参数值都是“对象”
Ray,2010年

11

装箱和拆箱专门用于将值类型的对象视为引用类型。将其实际值移动到托管堆并通过引用访问其值。

如果没有装箱和拆箱,则永远无法通过引用传递值类型。这意味着您不能将值类型作为Object的实例传递。


经过近10年的先生+1仍然很不错的答案
snr

1
通过数字类型的引用传递存在于没有装箱的语言中,其他语言实现了将值类型视为对象的实例,而没有装箱并将值移动到堆中(例如,动态语言的实现将指针对齐到4个字节边界使用低四位引用位,指示该值是整数或符号而不是完整对象;此类值类型是不可变的,并且大小与指针相同。
Pete Kirkham

8

我最后要拆箱的地方是在编写一些代码以从数据库中检索某些数据时(我没有使用LINQ to SQL,而只是普通的ADO.NET):

int myIntValue = (int)reader["MyIntValue"];

基本上,如果您使用的是泛型之前的旧版API,则会遇到装箱问题。除此之外,它并不常见。


4

当我们有一个需要将对象作为参数的函数但需要传递不同的值类型时,则需要装箱。在这种情况下,我们需要先将值类型转换为对象数据类型,然后再将其传递给函数。

我认为这不是真的,请尝试以下方法:

class Program
    {
        static void Main(string[] args)
        {
            int x = 4;
            test(x);
        }

        static void test(object o)
        {
            Console.WriteLine(o.ToString());
        }
    }

一切正常,我没有使用装箱/拆箱。(除非编译器在后台执行此操作?)


那是因为一切都从System.Object继承,并且您为该方法提供了一个带有附加信息的对象,因此基本上,您在调用test方法时使用了它所期望的内容以及它可能期望的任何内容,因为它没有特别的期望。.NET在幕后做了很多工作,以及使用这种语言非常简单的原因
Mikael Puusaari

1

在.net中,Object的每个实例或从其派生的任何类型都包括一个数据结构,该数据结构包含有关其类型的信息。.net中的“实际”值类型不包含任何此类信息。为了允许期望接收从对象派生的类型的例程处理值类型中的数据,系统会自动为每个值类型定义具有相同成员和字段的对应类类型。装箱会创建此类类的新实例,并从值类型实例中复制字段。取消装箱将字段从类类型的实例复制到值类型的实例。从值类型创建的所有类类型都源自具有讽刺意味的名称类ValueType(尽管其名称实际上是引用类型)。


0

当某个方法仅将引用类型作为参数时(例如,通过 new),您将无法将引用类型传递给它,而必须将其装箱。

这也是该采取任何方法,真正object作为一个参数-这将是引用类型。


0

通常,您通常希望避免将值类型装箱。

但是,在少数情况下有用。例如,如果您需要针对1.1框架,则将无权访问通用集合。.NET 1.1中对集合的任何使用都需要将您的值类型视为System.Object,这会导致装箱/拆箱。

仍然有一些情况在.NET 2.0+中有用。每当您想利用所有类型(包括值类型)都可以直接作为对象的事实时,您可能需要使用装箱/拆箱。有时这会很方便,因为它允许您保存集合中的任何类型(通过在通用集合中使用object而不是T),但是通常最好避免这种情况,因为这样会丢失类型安全性。但是,经常发生装箱的一种情况是在使用Reflection时-使用值类型时,反射中的许多调用都需要装箱/拆箱,因为该类型是事先未知的。


0

装箱是将值转换为引用类型,并且数据在堆上的对象中处于某个偏移位置。

至于拳击实际上是做什么的。这里有些例子

单声道C ++

void* mono_object_unbox (MonoObject *obj)
 {    
MONO_EXTERNAL_ONLY_GC_UNSAFE (void*, mono_object_unbox_internal (obj));
 }

#define MONO_EXTERNAL_ONLY_GC_UNSAFE(t, expr) \
    t result;       \
    MONO_ENTER_GC_UNSAFE;   \
    result = expr;      \
    MONO_EXIT_GC_UNSAFE;    \
    return result;

static inline gpointer
mono_object_get_data (MonoObject *o)
{
    return (guint8*)o + MONO_ABI_SIZEOF (MonoObject);
}

#define MONO_ABI_SIZEOF(type) (MONO_STRUCT_SIZE (type))
#define MONO_STRUCT_SIZE(struct) MONO_SIZEOF_ ## struct
#define MONO_SIZEOF_MonoObject (2 * MONO_SIZEOF_gpointer)

typedef struct {
    MonoVTable *vtable;
    MonoThreadsSync *synchronisation;
} MonoObject;

在Mono中取消装箱是一种在对象中偏移2个gpointer(例如16个字节)的指针的过程。A gpointer是一个void*。在查看的定义时,这很有意义,MonoObject因为它显然只是数据的标头。

C ++

要在C ++中装箱值,您可以执行以下操作:

#include <iostream>
#define Object void*

template<class T> Object box(T j){
  return new T(j);
}

template<class T> T unbox(Object j){
  T temp = *(T*)j;
  delete j;
  return temp;
}

int main() {
  int j=2;
  Object o = box(j);
  int k = unbox<int>(o);
  std::cout << k;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.