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

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

我知道什么是装箱和开箱,但我不理解它的真正用途。为什么,在哪里使用?

short s = 25;


object objshort = s;  //Boxing


short anothershort = (short)objshort;  //Unboxing
157750 次浏览

Boxing和Unboxing专门用于将值类型对象视为引用类型;将它们的实际值移动到托管堆并通过引用访问它们的值。

如果不进行装箱和拆箱,就无法通过引用传递值类型;这意味着您不能将值类型作为Object的实例传递。

为什么

拥有统一的类型系统,并允许值类型以与引用类型完全不同的方式表示其底层数据(例如,int只是一个32位的桶,这与引用类型完全不同)。

这样想。你有一个类型为object的变量o。现在你有一个int,你想把它放到o中。o是对某处某物的引用,而int显然不是对某处某物的引用(毕竟,它只是一个数字)。因此,你要做的是:创建一个新的object来存储int,然后将该对象的引用赋值给o。我们称这个过程为“装箱”。

所以,如果你不关心是否有一个统一的类型系统(例如,引用类型和值类型有非常不同的表示,你不想用一种通用的方式来“表示”这两者),那么你就不需要装箱。如果你不关心让int表示它们的底层值(即,相反,让int也是引用类型,只存储对它们的底层值的引用),那么你不需要装箱。

我应该在哪里使用它。

例如,旧的集合类型ArrayList只吃__abc1。也就是说,它只存储对某个地方的某个对象的引用。如果没有装箱,就不能将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!装箱转换生成被装箱的值的副本,以解释行为上的差异。

最后一个我必须开箱的地方是当我写一些代码,从数据库检索一些数据(我没有使用LINQ 用SQL,只是普通的旧ADO。网):

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

基本上,如果使用泛型之前的旧api,就会遇到装箱。除此之外,这种情况并不常见。

装箱并不是你真正使用的东西——它是运行时使用的东西,这样你就可以在必要时以同样的方式处理引用和值类型。例如,如果您使用ArrayList来保存整数列表,则整数将被装箱以适应ArrayList中的对象类型插槽。

现在使用泛型集合,这种情况几乎消失了。如果你创建了一个List<int>,就没有装箱——List<int>可以直接保存整数。

当一个方法只接受一个引用类型作为参数时(比如一个泛型方法通过new约束约束为一个类),你将不能将引用类型传递给它,必须对它进行装箱。

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

在. net框架中,有两种类型——值类型和引用类型。这在面向对象语言中比较常见。

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

现在,回到过去(Microsoft.NET 1.0),还没有这种新奇的泛型喧闹。您不能编写一个方法,它只有一个参数,可以服务于值类型和引用类型。这违反了多态性。因此,采用装箱作为一种将值类型强制转换为对象的方法。

如果这是不可能的,那么框架中就会充斥着方法和类,它们的唯一目的就是接受其他类型。不仅如此,由于值类型并没有真正共享一个共同的类型祖先,因此必须为每种值类型(比特、字节、int16、int32等等)设置不同的方法重载。

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

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

当我们有一个需要object作为参数的函数,但我们有不同的值类型需要传递时,装箱是必需的,在这种情况下,我们需要在将值类型传递给函数之前首先将值类型转换为对象数据类型。

我不认为这是真的,试试这个:

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


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

这运行得很好,我没有使用装箱/拆箱。(除非编译器在幕后做这些?)

一般来说,您通常会希望避免对值类型进行装箱。

然而,在极少数情况下,这是有用的。例如,如果您需要以1.1框架为目标,那么您将无法访问泛型集合。在. net 1.1中使用任何集合都需要将您的值类型视为系统。对象,该对象导致装箱/解装箱。

在。net 2.0+中仍然有一些情况可以证明这一点。任何时候,如果您想利用所有类型(包括值类型)都可以直接作为对象处理的事实,您可能需要使用装箱/拆箱。这有时很方便,因为它允许您保存集合中的任何类型(通过在泛型集合中使用object而不是T),但通常情况下,最好避免这样做,因为您会失去类型安全性。不过,装箱频繁发生的一种情况是在使用反射时——在处理值类型时,反射中的许多调用都需要装箱/拆箱,因为类型是事先不知道的。

理解这一点的最好方法是看看c#所基于的底层编程语言。

在像C这样的最低级语言中,所有变量都放在一个地方:堆栈。每次你声明一个变量,它就会被放到堆栈上。它们只能是基本值,比如bool、字节、32位整型、32位uint等。Stack既简单又快速。当变量被添加时,它们就会一个一个地放在另一个上面,所以你在RAM中声明的第一个位于0x00,下一个位于0x01,下一个位于0x02,等等。此外,变量通常在编译时预先寻址,因此在运行程序之前就知道它们的地址。

在上一层中,像c++一样,引入了第二个称为堆的内存结构。你仍然大部分生活在堆栈中,但特殊的int称为指针可以添加到堆栈中,它存储对象的第一个字节的内存地址,并且该对象生活在堆中。Heap有点混乱,维护起来有点昂贵,因为与Stack变量不同,它们不会在程序执行时线性地向上和向下堆积。它们的来去没有特定的顺序,它们可以生长和收缩。

处理指针是很困难的。它们是导致内存泄漏、缓冲区溢出和失败的原因。c#来拯救。

在更高的层次上,c#,你不需要考虑指针——. net框架(用c++编写)为你考虑这些,并将它们作为对象引用呈现给你,为了性能,让你将更简单的值,如bool, bytes和int作为值类型存储。在底层,对象和实例化类的东西放在昂贵的内存管理堆上,而值类型放在低级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是否为空,因为bool值已经被装箱放在那里了。

一般来说,你应该避免装箱,除非你需要它,例如传递一个int/bool/任何作为对象的参数。. net中的一些基本结构仍然需要将值类型作为对象传递(因此需要Box),但在大多数情况下,您不应该需要Box。

一个不详尽的历史c#结构列表,需要Boxing,你应该避免:

  • 事件系统原来有一个竞争条件在幼稚的使用它,它不支持异步。再加上Boxing问题,它应该被避免。(例如,您可以将其替换为使用泛型的异步事件系统。)

  • 旧的Threading和Timer模型在参数上强制使用Box,但已经被async/await所取代,后者更干净、更高效。

  • . net 1.1集合完全依赖于Boxing,因为它们出现在泛型之前。这些仍然在System.Collections中。在任何新代码中,你都应该使用System.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,没有任何功能上的区别。

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

至于拳击到底是做什么的。下面是一些例子

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_unbox_internal (MonoObject *obj)
{
/* add assert for valuetypes? */
g_assert (m_class_is_valuetype (mono_object_class (obj)));
return mono_object_get_data (obj);
}


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字节)。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;
}

当值类型传递给类型为object的变量或参数时,就会发生装箱。因为它是自动发生的,所以问题不是什么时候应该使用装箱,而是什么时候应该使用object类型。

object类型应该只在绝对必要时使用,因为它规避了类型安全,而类型安全是c#等静态类型语言的主要好处。但是在不可能在编译时知道值的类型的情况下,它可能是必要的。

例如,当通过ADO读取数据库字段值时。净框架。返回值可以是整数、字符串或其他类型,所以类型必须是object,客户端代码必须执行适当的强制转换。为了避免这个问题,ORM框架如Linq-to-SQL或EF Core使用静态类型的实体,因此避免使用object

在引入泛型之前,像ArrayList这样的集合的项类型是object。这意味着您可以在列表中存储任何内容,并且可以向数字列表中添加字符串,而不会引起类型系统的抱怨。泛型解决了这个问题,在使用值类型集合时不再需要装箱。

因此,很少需要键入object这样的东西,你想要避免它。在代码需要同时处理值类型和引用类型的情况下,泛型通常是更好的解决方案。