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

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

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing

当前回答

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

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

其他回答

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

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

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

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

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

最后一个我不得不开箱的地方是从数据库中检索数据的代码(我没有使用LINQ to SQL,只是简单的旧ADO.NET):

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

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

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

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

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

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

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

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

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

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

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

In the lowest-level languages like C, all variables go one place: The Stack. Each time you declare a variable it goes on the Stack. They can only be primitive values, like a bool, a byte, a 32-bit int, a 32-bit uint, etc. The Stack is both simple and fast. As variables are added they just go one on top of another, so the first you declare sits at say, 0x00, the next at 0x01, the next at 0x02 in RAM, etc. In addition, variables are often pre-addressed at compile-time, so their address is known before you even run the program.

In the next level up, like C++, a second memory structure called the Heap is introduced. You still mostly live in the Stack, but special ints called Pointers can be added to the Stack, that store the memory address for the first byte of an Object, and that Object lives in the Heap. The Heap is kind of a mess and somewhat expensive to maintain, because unlike Stack variables they don't pile linearly up and then down as a program executes. They can come and go in no particular sequence, and they can grow and shrink.

处理指针是很困难的。它们是导致内存泄漏、缓冲区溢出和失败的原因。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,你应该避免:

The Event system turns out to have a Race Condition in naive use of it, and it doesn't support async. Add in the Boxing problem and it should probably be avoided. (You could replace it for example with an async event system that uses Generics.) The old Threading and Timer models forced a Box on their parameters but have been replaced by async/await which are far cleaner and more efficient. The .Net 1.1 Collections relied entirely on Boxing, because they came before Generics. These are still kicking around in System.Collections. In any new code you should be using the Collections from System.Collections.Generic, which in addition to avoiding Boxing also provide you with stronger type-safety.

你应该避免声明或传递你的值类型作为对象,除非你必须处理上述强制装箱的历史问题,并且你想避免在知道它无论如何都会被装箱时对它进行装箱的性能影响。

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