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

Posted

技术标签:

【中文标题】为啥我们需要在 C# 中装箱和拆箱?【英文标题】:Why do we need boxing and unboxing in C#?为什么我们需要在 C# 中装箱和拆箱? 【发布时间】:2011-01-07 21:14:39 【问题描述】:

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

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

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing

【问题讨论】:

【参考方案1】:

为什么

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

这样想。你有一个object 类型的变量o。现在你有一个int,你想把它放入oo 是对某处某事的引用,int 强调不是对某处某事的引用(毕竟,它只是一个数字)。所以,你要做的是:创建一个新的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);

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

【讨论】:

@Jason 你的意思是说如果我们有原始列表,就没有理由使用任何装箱/拆箱? 我不确定您所说的“原始列表”是什么意思。 能否谈谈boxingunboxing 对性能的影响? @KevinMeredith 在msdn.microsoft.com/en-us/library/ms173196.aspx中有一个关于装箱和拆箱操作性能的基本解释 优秀的答案——比我在知名书籍中读到的大多数解释都要好。【参考方案2】:

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

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

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

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

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

【讨论】:

在泛型之前,很多事情都需要自动装箱;然而,鉴于泛型的存在,如果不是为了保持与旧代码的兼容性,我认为 .net 最好没有隐含的装箱转换。将List<string>.Enumerator 之类的值类型转换为IEnumerator<string> 会生成一个对象,该对象的行为主要类似于类类型,但Equals 方法已损坏。将List<string>.Enumerator 转换为IEnumerator<string> 的更好方法是调用自定义转换运算符,但隐含转换的存在阻止了这一点。【参考方案3】:

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

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

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

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

在更高的层次上,C#,您不需要考虑指针 - .Net 框架(用 C++ 编写)会为您考虑这些并将它们作为对对象的引用呈现给您,并且为了性能,让您将更简单的值(如 bool、bytes 和 ints)存储为值类型。在底层,实例化类的对象和东西放在昂贵的内存管理堆上,而值类型放在与低级 C 相同的堆栈中 - 超级快。

从编码人员的角度来看,为了使这两个根本不同的内存概念(和存储策略)之间的交互保持简单,值类型可以随时被装箱。 Boxing causes the value to be copied from the Stack, put in an Object, and placed on the Heap - 更昂贵,但与参考世界的流畅交互。正如其他答案所指出的那样,例如,当您说:

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 中有一些基本结构仍然需要将值类型作为对象传递(因此需要装箱),但在大多数情况下,您永远不需要装箱。

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

事件系统turns out to have a Race Condition 天真地使用它,它不支持异步。加上拳击问题,应该可以避免。 (例如,您可以将其替换为使用泛型的异步事件系统。)

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

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

【讨论】:

你教会了我一百个程序员和 IT 专业人士多年来无法解释的事情,但将其改为说你应该做什么而不是避免做什么,因为它有点难以遵循.. 基本规则通常不会 1. 你不应该这样做,而是这样做 这个答案应该已经被标记为 ANSWER 一百次了! c#中没有“Int”,有int和Int32。我相信你说一个是值类型而另一个是包装值类型的引用类型是错误的。除非我弄错了,否则这在 Java 中是正确的,但不是 C#。在 C# 中,IDE 中显示为蓝色的那些是其结构定义的别名。所以:int = Int32,bool = Boolean,string = String。使用 bool 而不是 Boolean 的原因是因为在 MSDN 设计指南和约定中建议这样做。否则我喜欢这个答案。但我会拒绝投票,直到你证明我错了或在你的答案中解决这个问题。 如果您将一个变量声明为 int,而将另一个变量声明为 Int32,或者 bool 和 Boolean - 右键单击​​并查看定义,您最终会得到相同的结构定义。 @HeribertoLugo 是正确的,“您应该避免将值类型声明为 Bool 而不是 bool”这一行是错误的。正如 OP 指出的那样,您应该避免将 bool(或 Boolean 或任何其他值类型)声明为 Object。 bool/Boolean、int/Int32 只是 C# 和 .NET 之间的别名:docs.microsoft.com/en-us/dotnet/csharp/language-reference/…【参考方案4】:

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

现在使用泛型集合,这几乎消失了。如果您创建一个List&lt;int&gt;,则不会进行装箱 - List&lt;int&gt; 可以直接保存整数。

【讨论】:

对于复合字符串格式等问题,您仍然需要装箱。使用泛型时您可能不会经常看到它,但它肯定仍然存在。 true - 它也一直出现在 ADO.NET 中 - 无论实际数据类型是什么,sql 参数值都是“对象”【参考方案5】:

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

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

【讨论】:

经过将近 10 年的先生 +1 仍然是一个不错的答案 通过引用传递数字类型存在于没有装箱的语言中,而其他语言实现将值类型视为 Object 的实例而不装箱并将值移动到堆中(例如,指针对齐的动态语言的实现到 4 字节边界使用引用的低四位来指示该值是整数或符号而不是完整对象;此类值类型是不可变的,并且与指针大小相同。【参考方案6】:

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

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

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

【讨论】:

【参考方案7】:

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

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

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

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

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

【讨论】:

那是因为一切都从 System.Object 继承,并且您为该方法提供了一个带有额外信息的对象,所以基本上您正在调用测试方法,其中包含它所期望的以及它可能期望的任何东西,因为它没有什么特别的期待。 .NET 中的很多工作都是在幕后完成的,以及它是一种非常易于使用的语言的原因【参考方案8】:

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

【讨论】:

【参考方案9】:

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

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

【讨论】:

【参考方案10】:

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

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

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

【讨论】:

【参考方案11】:

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

至于拳击的实际作用。下面是一些例子

单声道 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;

【讨论】:

【参考方案12】:

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

object 类型只应在绝对必要时使用,因为它规避了类型安全性,而类型安全性是 C# 等静态类型语言的主要优点。但在编译时无法知道值的类型的情况下,可能需要这样做。

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

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

因此,很少需要输入 object 之类的内容,并且您想避免它。在代码需要能够同时处理值类型和引用类型的情况下,泛型通常是更好的解决方案。

【讨论】:

以上是关于为啥我们需要在 C# 中装箱和拆箱?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我们在 Java 中使用自动装箱和拆箱?

c# 泛型为啥能解决装箱拆箱问题

C#装箱和拆箱

[C#] 装箱和拆箱(整理中)

C# 装箱和拆箱的简单理解

转 C# 装箱和拆箱[整理]