为什么我们需要在C#中进行装箱和拆箱?

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

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing


#1 楼


为什么


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

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

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


我应该在哪里使用它。


例如,旧的集合类型ArrayList只吃object。也就是说,它仅存储对居住在某处的事物的引用。如果没有装箱,则无法将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);


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

评论


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

–起搏器
2012年3月7日18:05



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

–杰森
2012年3月7日19:38

您能否谈一下装箱和拆箱对性能的影响?

–凯文·梅瑞迪斯(Kevin Meredith)
2013年6月7日12:36



@KevinMeredith在msdn.microsoft.com/zh-cn/library/ms173196.aspx中有关于装箱和拆箱操作性能的基本说明

– InfZero
2014年1月1日19:46

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

– FredM
16年4月22日在21:26

#2 楼

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

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

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

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

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

评论


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

–超级猫
2012-09-26 23:21

#3 楼

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

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

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

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

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

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

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


拳击的好处的一个有力例证就是检查null:

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


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

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

历史的非详尽列表需要避免拳击的C#结构,您应该避免:


事件系统原来在其天真使用时就具有竞态条件,并且不支持异步。添加拳击问题,应该避免。 (例如,您可以将其替换为使用Generics的异步事件系统。)
旧的Threading和Timer模型在其参数上强制使用Box,但已由async / await替换,这更加干净和高效。
.Net 1.1集合完全依赖于Boxing,因为它们早于泛型。这些仍在System.Collections中启动。在任何新代码中,都应该使用System.Collections.Generic中的Collections,除了避免Boxing之外,它还为您提供了更强的类型安全性。

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

每个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,而没有任何功能上的区别。

评论


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

– Mikael Puusaari
16年5月23日在18:03

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

–蒲颜
16-10-16在19:30

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

– Heriberto Lugo
18年1月21日在5:50



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

– Heriberto Lugo
18年1月21日在5:55

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

– STW
18年1月29日在14:51



#4 楼

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

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

评论


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

– Jeremy S
2010年1月21日,19:07

true-它也始终在ADO.NET中显示-sql参数值都是“对象”,无论实际数据类型是什么

–雷
2010年1月21日,19:12

#5 楼

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

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

评论


差不多十年了,先生+1还是不错的答案

–snr
20-2-12在7:28

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

– Pete Kirkham
20年6月1日在16:36

#6 楼

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

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


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

#7 楼

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

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
16年5月23日在0:28



#8 楼

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

#9 楼

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

对于将object作为参数的任何方法也是如此-这必须是引用类型。

#10 楼

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

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

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

#11 楼

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

关于装箱的实际作用。以下是一些示例

Mono 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个gpointers的位置投射一个指针的过程。 (例如16个字节)。 gpointervoid*。在查看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;
}