365

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

我知道什么是装箱和拆箱,但我无法理解它的真正用途。为什么以及在哪里使用它?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing
4

12 回答 12

520

为什么

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

像这样想。你有一个o类型的变量object。现在你有一个int并且你想把它放进去oo是对某物的引用,而 theint强调不是对某物的引用(毕竟,它只是一个数字)。因此,您要做的是:创建一个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是a,struct那么输出是1,但如果Point是a,class那么输出是2!装箱转换会复制被装箱的值,以解释行为差异。

于 2010-01-21T18:43:22.337 回答
68

在 .NET 框架中,有两种类型——值类型和引用类型。这在 OO 语言中比较常见。

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

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

如果这是不可能的,那么框架就会到处都是方法和类,它们的唯一目的是接受其他类型的类型。不仅如此,由于值类型并不真正共享一个共同的类型祖先,因此您必须为每种值类型(位、字节、int16、int32 等)使用不同的方法重载。

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

于 2010-01-21T18:53:54.367 回答
64

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

在像 C 这样的最低级语言中,所有变量都集中在一个地方:堆栈。每次您声明一个变量时,它都会进入堆栈。它们只能是原始值,如布尔值、字节、32 位 int、32 位 uint 等。堆栈既简单又快速。随着变量的添加,它们只是一个在另一个之上,所以你声明的第一个位于 0x00,下一个位于 0x01,下一个位于 RAM 中的 0x02,等等。此外,变量通常在编译时预先寻址 -时间,所以他们的地址在你运行程序之前就已经知道了。

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

处理指针很难。它们是内存泄漏、缓冲区溢出和挫折的原因。C# 来救援。

在更高的层次上,C#,您不需要考虑指针 - .Net 框架(用 C++ 编写)会为您考虑这些并将它们作为对对象的引用呈现给您,并且为了提高性能,您可以存储更简单的值像 bools、bytes 和 ints 作为值类型。在底层,实例化一个类的对象和东西放在昂贵的内存管理堆上,而值类型放在与低级 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 在技术上是堆栈中的一个地址,它指向我们的布尔 b 的副本,该副本已被复制到堆中。我们可以检查 o 是否为 null,因为 bool 已被装箱并放在那里。

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

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

  • 事件系统在天真地使用它时有一个竞争条件,它不支持异步。加上拳击问题,应该可以避免。(例如,您可以将其替换为使用泛型的异步事件系统。)

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

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

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

根据 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,没有任何功能差异。

于 2016-02-12T21:38:45.517 回答
27

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

现在使用泛型集合,这几乎消失了。如果您创建 a List<int>,则没有完成装箱 -List<int>可以直接保存整数。

于 2010-01-21T18:43:59.397 回答
15

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

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

于 2010-01-21T18:42:19.240 回答
8

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

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

基本上,如果您在泛型之前使用较旧的 API,您会遇到拳击。除此之外,它并不常见。

于 2010-01-21T18:43:26.777 回答
3

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

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

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

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

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

于 2011-10-17T05:01:35.477 回答
2

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

于 2011-09-13T06:43:34.337 回答
0

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

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

于 2010-01-21T18:44:51.053 回答
0

通常,您通常希望避免对值类型进行装箱。

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

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

于 2013-10-31T20:14:05.780 回答
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_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 个字节)的过程。Agpointer是一个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;
}
于 2020-06-01T16:24:23.750 回答
0

装箱发生在将值类型传递给类型为 的变量或参数时object。由于它是自动发生的,所以问题不在于何时应该使用拳击,而是何时应该使用 type object

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

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

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

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

于 2021-11-18T10:43:13.170 回答