为什么在c#中需要装箱和拆箱?
我知道什么是装箱和开箱,但我不理解它的真正用途。为什么,在哪里使用?
short s = 25;
object objshort = s; //Boxing
short anothershort = (short)objshort; //Unboxing
为什么在c#中需要装箱和拆箱?
我知道什么是装箱和开箱,但我不理解它的真正用途。为什么,在哪里使用?
short s = 25;
object objshort = s; //Boxing
short anothershort = (short)objshort; //Unboxing
当前回答
装箱是将值转换为引用类型,其中数据位于堆上对象的某个偏移量处。
至于拳击到底是做什么的。下面是一些例子
单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;
}
其他回答
为什么
拥有统一的类型系统,并允许值类型以与引用类型完全不同的方式表示其基础数据(例如,int只是一个32位的桶,这与引用类型完全不同)。
这样想。你有一个object类型的变量o。现在你有一个int型,你想把它变成o。o是对某处某物的引用,而int型显然不是对某处某物的引用(毕竟,它只是一个数字)。所以,你要做的是:创建一个可以存储int的新对象,然后将该对象的引用赋值给o。我们称这个过程为“装箱”。
所以,如果你不关心是否有一个统一的类型系统(例如,引用类型和值类型有非常不同的表示,你不想用一种通用的方式来“表示”这两者),那么你就不需要装箱。如果你不关心用int表示它们的底层值(也就是说,用int作为引用类型,只存储对它们底层值的引用),那么你不需要装箱。
我应该在哪里使用它。
例如,旧的集合类型ArrayList只包含对象。也就是说,它只存储对某个地方的某个对象的引用。如果没有装箱,就不能在这样的集合中放入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);
下句话之前先想一下。
如果你说真假太棒了!等等,什么?这是因为引用类型上的==使用引用相等来检查引用是否相等,而不是检查底层值是否相等。这是一个很容易犯的危险错误。也许更微妙
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是一个结构体,则输出为1,但如果Point是一个类,则输出为2!装箱转换生成被装箱的值的副本,以解释行为上的差异。
最后一个我不得不开箱的地方是从数据库中检索数据的代码(我没有使用LINQ to SQL,只是简单的旧ADO.NET):
int myIntValue = (int)reader["MyIntValue"];
基本上,如果使用泛型之前的旧api,就会遇到装箱。除此之外,这种情况并不常见。
装箱是将值转换为引用类型,其中数据位于堆上对象的某个偏移量处。
至于拳击到底是做什么的。下面是一些例子
单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,没有任何功能上的区别。
当我们有一个需要object作为参数的函数,但我们有不同的值类型需要传递时,装箱是必需的,在这种情况下,我们需要在将值类型传递给函数之前首先将值类型转换为对象数据类型。
我不认为这是真的,试试这个:
class Program
{
static void Main(string[] args)
{
int x = 4;
test(x);
}
static void test(object o)
{
Console.WriteLine(o.ToString());
}
}
这运行得很好,我没有使用装箱/拆箱。(除非编译器在幕后做这些?)