为什么 C# 中需要装箱和拆箱?
为什么 C# 中需要装箱和拆箱?
我知道什么是装箱和拆箱,但我无法理解它的真正用途。为什么以及在哪里应该使用它?
short s = 25;
object objshort = s; //Boxing
short anothershort = (short)objshort; //Unboxing
Why do we need boxing and unboxing in C#?
I know what boxing and unboxing is, but I can't comprehend the real use of it. Why and where should I use it?
short s = 25;
object objshort = s; //Boxing
short anothershort = (short)objshort; //Unboxing
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(12)
当我们有一个需要对象作为参数的函数,但我们需要传递不同的值类型时,需要装箱,在这种情况下,我们需要首先将值类型转换为对象数据类型,然后再将其传递给函数。
我不认为这是真的,试试这个:
运行得很好,我没有使用装箱/拆箱。 (除非编译器在幕后执行此操作?)
Boxing is required, when we have a function that needs object as a parameter, but we have different value types that need to be passed, in that case we need to first convert value types to object data types before passing it to the function.
I don't think that is true, try this instead:
That runs just fine, I didn't use boxing/unboxing. (Unless the compiler does that behind the scenes?)
在 .net 中,Object 的每个实例或从其派生的任何类型都包含一个数据结构,其中包含有关其类型的信息。 .net 中的“真实”值类型不包含任何此类信息。为了允许期望接收从对象派生的类型的例程操作值类型中的数据,系统自动为每个值类型定义具有相同成员和字段的相应类类型。装箱创建此类类型的新实例,并从值类型实例复制字段。拆箱将字段从类类型的实例复制到值类型的实例。所有从值类型创建的类类型都派生自具有讽刺意味的名称类 ValueType(尽管它的名称如此,但它实际上是一个引用类型)。
In .net, every instance of Object, or any type derived therefrom, includes a data structure which contains information about its type. "Real" value types in .net do not contain any such information. To allow data in value types to be manipulated by routines that expect to receive types derived from object, the system automatically defines for each value type a corresponding class type with the same members and fields. Boxing creates a new instances of this class type, copying the fields from a value type instance. Unboxing copies the fields from an instance of the class type to an instance of the value type. All of the class types which are created from value types are derived from the ironically named class ValueType (which, despite its name, is actually a reference type).
装箱是将值转换为引用类型,数据位于堆上对象的某个偏移处。
至于拳击实际上是做什么的。以下是一些示例
Mono C++
在 Mono 中拆箱对象是在对象中 2 个 gpointers 的偏移量(例如 16 个字节)处转换指针的过程。
gpointer
是一个void*
。在查看MonoObject
的定义时,这是有道理的,因为它显然只是数据的标头。C++
要在 C++ 中对值进行装箱,您可以执行以下操作:
Boxing is the conversion of a value to a reference type with the data at some offset in an object on the heap.
As for what boxing actually does. Here are some examples
Mono C++
Unboxing an object in Mono is a process of casting a pointer at an offset of 2 gpointers in the object (e.g. 16 bytes). A
gpointer
is avoid*
. This makes sense when looking at the definition ofMonoObject
as it's clearly just a header for the data.C++
To box a value in C++ you could do something like:
当方法仅采用引用类型作为参数时(例如通过 new 约束将泛型方法限制为类),您将无法将引用类型传递给它并且必须装箱它。
对于任何采用
object
作为参数的方法也是如此 - 这将具有作为引用类型。When a method only takes a reference type as a parameter (say a generic method constrained to be a class via the
new
constraint), you will not be able to pass a reference type to it and have to box it.This is also true for any methods that take
object
as a parameter - this will have to be a reference type.一般来说,您通常希望避免对值类型进行装箱。
然而,在极少数情况下这是有用的。例如,如果您需要以 1.1 框架为目标,您将无法访问通用集合。在 .NET 1.1 中对集合的任何使用都需要将值类型视为 System.Object,这会导致装箱/拆箱。
在某些情况下,这在 .NET 2.0+ 中仍然有用。任何时候您想要利用所有类型(包括值类型)都可以直接视为对象的事实,您可能需要使用装箱/拆箱。这有时很方便,因为它允许您保存集合中的任何类型(通过在泛型集合中使用对象而不是 T),但一般来说,最好避免这种情况,因为您会失去类型安全性。然而,频繁发生装箱的一种情况是当您使用反射时 - 在处理值类型时,反射中的许多调用将需要装箱/拆箱,因为事先不知道该类型。
In general, you typically will want to avoid boxing your value types.
However, there are rare occurances where this is useful. If you need to target the 1.1 framework, for example, you will not have access to the generic collections. Any use of the collections in .NET 1.1 would require treating your value type as a System.Object, which causes boxing/unboxing.
There are still cases for this to be useful in .NET 2.0+. Any time you want to take advantage of the fact that all types, including value types, can be treated as an object directly, you may need to use boxing/unboxing. This can be handy at times, since it allows you to save any type in a collection (by using object instead of T in a generic collection), but in general, it is better to avoid this, as you're losing type safety. The one case where boxing frequently occurs, though, is when you're using Reflection - many of the calls in reflection will require boxing/unboxing when working with value types, since the type is not known in advance.
当值类型传递给对象类型的变量或参数时,就会发生装箱。由于它是自动发生的,所以问题不在于何时应该使用装箱,而在于何时应该使用
object
类型。object
类型仅应在绝对必要时使用,因为它规避了类型安全性,而类型安全性是 C# 等静态类型语言的主要优点。但如果在编译时无法知道值的类型,则可能有必要这样做。例如,通过 ADO.NET 框架读取数据库字段值时。返回的值可以是整数、字符串或其他值,因此类型必须是
object
,并且客户端代码必须执行适当的转换。为了避免此问题,Linq-to-SQL 或 EF Core 等 ORM 框架改用静态类型实体,因此避免使用object
。在引入泛型之前,像
ArrayList
这样的集合的项目类型为object
。这意味着您可以在列表中存储任何内容,并且可以将字符串添加到数字列表中,而类型系统不会抱怨。泛型解决了这个问题,并且在使用值类型集合时使装箱变得不必要。因此,很少需要将某些内容输入为
object
,并且您希望避免这样做。在代码需要能够处理值类型和引用类型的情况下,泛型通常是更好的解决方案。Boxing happens when a value type is passed to a variable or parameter with a type of
object
. Since it happens automatically, the question is not when you should use boxing, but rather when you should use the typeobject
.The type
object
should only be used when it is absolutely necessary, since it circumvents the type safety which is otherwise a major benefit of a statically typed language like C#. But it may be necessary in cases where it is not possible to know the type of a value at compile time.For example when reading a database field value through the ADO.NET framework. The returned value could be either an integer or a string or something else, so the type has to be
object
, and the client code has to perform the appropriate casting. To avoid this problem, ORM frameworks like Linq-to-SQL or EF Core use statically typed entities instead, so the use ofobject
is avoided.Before the introduction of generics, collections like
ArrayList
had the items types asobject
. This meant you could store anything in a list, and you could add a string to a list of numbers, without the type system complaining. Generics solve this problem and make boxing unnecessary when using collections of value types.So typing something as
object
is rarely needed, and you want to avoid it. Generics is typically a better solution in cases where code needs to be able to handle both value types and reference types.要拥有一个统一的类型系统,并允许值类型对其底层数据的表示方式与引用类型表示其底层数据的方式完全不同(例如,
int
只是一桶 30-两位与引用类型完全不同)。这样想吧。您有一个
object
类型的变量o
。现在您有一个int
并且您想将其放入o
中。o
是对某个地方的引用,而int
显然不是对某个地方的引用(毕竟,它只是一个数字)。因此,您要做的是:创建一个可以存储int
的新对象
,然后将该对象的引用分配给o
。我们称这个过程为“拳击”。因此,如果您不关心是否有一个统一的类型系统(即,引用类型和值类型具有非常不同的表示形式,并且您不想要一种通用的方式来“表示”这两者),那么您就不需要装箱。如果您不关心让
int
代表其底层值(即,让int
也成为引用类型并仅存储对其底层值的引用),那么您就不需要不需要拳击。例如,旧的集合类型
ArrayList
只吃object
。也就是说,它只存储对存在于某处的事物的引用。如果没有装箱,您就无法将int
放入这样的集合中。但通过拳击,你可以。现在,在仿制药时代,您实际上并不需要这个,并且通常可以愉快地进行而不用考虑这个问题。但有一些注意事项需要注意:
这是正确的:
这是不正确的:
相反,您必须这样做:
首先,我们必须显式取消装箱
double
((double)o),然后将其转换为
int
。以下结果是什么:
在继续下一句话之前先想一想。
如果您说
True
和False
那就太棒了!等等,什么?这是因为引用类型上的==
使用引用相等来检查引用是否相等,而不是基础值是否相等。这是一个很容易犯的危险错误。也许更微妙的是还会打印
False
!更好的说法是:
幸运的是,它将打印
True
。最后一个微妙之处:
输出是什么?这取决于!如果
Point
是struct
,则输出为1
,但如果Point
是class
> 那么输出是2
!装箱转换会生成装箱值的副本,以解释行为的差异。To have a unified type system and allow value types to have a completely different representation of their underlying data from the way that reference types represent their underlying data (e.g., an
int
is just a bucket of thirty-two bits which is completely different than a reference type).Think of it like this. You have a variable
o
of typeobject
. And now you have anint
and you want to put it intoo
.o
is a reference to something somewhere, and theint
is emphatically not a reference to something somewhere (after all, it's just a number). So, what you do is this: you make a newobject
that can store theint
and then you assign a reference to that object too
. We call this process "boxing."So, if you don't care about having a unified type system (i.e., reference types and value types have very different representations and you don't want a common way to "represent" the two) then you don't need boxing. If you don't care about having
int
represent their underlying value (i.e., instead haveint
be reference types too and just store a reference to their underlying value) then you don't need boxing.For example, the old collection type
ArrayList
only eatsobject
s. That is, it only stores references to somethings that live somewhere. Without boxing you cannot put anint
into such a collection. But with boxing, you can.Now, in the days of generics you don't really need this and can generally go merrily along without thinking about the issue. But there are a few caveats to be aware of:
This is correct:
This is not:
Instead you must do this:
First we have to explicitly unbox the
double
((double)o
) and then cast that to anint
.What is the result of the following:
Think about it for a second before going on to the next sentence.
If you said
True
andFalse
great! Wait, what? That's because==
on reference types uses reference-equality which checks if the references are equal, not if the underlying values are equal. This is a dangerously easy mistake to make. Perhaps even more subtlewill also print
False
!Better to say:
which will then, thankfully, print
True
.One last subtlety:
What is the output? It depends! If
Point
is astruct
then the output is1
but ifPoint
is aclass
then the output is2
! A boxing conversion makes a copy of the value being boxed explaining the difference in behavior.在.NET框架中,有两种类型——值类型和引用类型。这在 OO 语言中比较常见。
面向对象语言的重要特性之一是能够以与类型无关的方式处理实例。这被称为多态性。由于我们想要利用多态性,但我们有两种不同的类型,因此必须有某种方法将它们组合在一起,以便我们可以以相同的方式处理其中一种。
现在,回到过去(Microsoft.NET 1.0),并没有这种新奇的泛型喧嚣。您无法编写具有可以为值类型和引用类型提供服务的单个参数的方法。这是违反多态性的。因此,采用装箱作为将值类型强制转换为对象的方法。
如果这是不可能的,框架将充斥着方法和类,其唯一目的是接受其他类型的类型。不仅如此,由于值类型并不真正共享公共类型祖先,因此您必须为每个值类型(位、字节、int16、int32 等)提供不同的方法重载。
拳击阻止了这种情况的发生。 这就是英国人庆祝节礼日的原因。
In the .NET framework, there are two species of types--value types and reference types. This is relatively common in OO languages.
One of the important features of object oriented languages is the ability to handle instances in a type-agnostic manner. This is referred to as polymorphism. Since we want to take advantage of polymorphism, but we have two different species of types, there has to be some way to bring them together so we can handle one or the other the same way.
Now, back in the olden days (1.0 of Microsoft.NET), there weren't this newfangled generics hullabaloo. You couldn't write a method that had a single argument that could service a value type and a reference type. That's a violation of polymorphism. So boxing was adopted as a means to coerce a value type into an object.
If this wasn't possible, the framework would be littered with methods and classes whose only purpose was to accept the other species of type. Not only that, but since value types don't truly share a common type ancestor, you'd have to have a different method overload for each value type (bit, byte, int16, int32, etc etc etc).
Boxing prevented this from happening. And that's why the British celebrate Boxing Day.
理解这一点的最佳方法是查看 C# 所基于的较低级编程语言。
在 C 等最低级语言中,所有变量都存放在一个地方:堆栈。每次声明一个变量时,它都会进入堆栈。它们只能是原始值,如 bool、byte、32 位 int、32 位 uint 等。堆栈既简单又快速。添加变量时,它们只是一个叠加另一个,因此您声明的第一个位于 RAM 中的 0x00,下一个位于 0x01,下一个位于 0x02,等等。此外,变量通常在编译时预先寻址。时间,因此在运行程序之前就知道它们的地址。
在下一个级别中,如 C++,引入了称为堆的第二个内存结构。您仍然主要居住在堆栈中,但是可以将称为指针的特殊整数添加到堆栈中,它们存储对象第一个字节的内存地址,并且该对象居住在堆中。堆有点混乱,而且维护起来有点昂贵,因为与堆栈变量不同,它们不会在程序执行时线性地向上然后向下堆积。它们可以没有特定的顺序来来去去,并且可以增长和收缩。
处理指针很困难。它们是内存泄漏、缓冲区溢出和失败的原因。 C# 来救援。
在更高的层次上,C#,您不需要考虑指针 - .Net 框架(用 C++ 编写)会为您考虑这些并将它们作为对象的引用呈现给您,并且为了性能,允许您存储更简单的值像 bool、bytes 和 ints 作为值类型。在底层,对象和实例化类的东西位于昂贵的内存管理堆中,而值类型位于与低级 C 中相同的堆栈中 - 速度超快。
从编码员的角度来看,为了保持这两个根本不同的内存概念(以及存储策略)之间的交互简单,可以随时对值类型进行装箱。 装箱会导致从堆栈中复制值,放入对象中,然后放置在堆 - 更昂贵,但是与参考世界的交互流畅。正如其他答案所指出的,当您说以下内容时,就会发生这种情况:
装箱优势的一个强有力的说明是对 null 的检查:
从技术上讲,我们的对象 o 是堆栈中的一个地址,它指向我们的 bool b 的副本,该地址已被复制到堆中。我们可以检查 o 是否为 null,因为布尔值已被装箱并放在那里。
一般来说,除非需要,否则应该避免装箱,例如将 int/bool/whatever 作为对象传递给参数。 .Net 中的一些基本结构仍然需要将值类型作为对象传递(因此需要装箱),但在大多数情况下,您永远不需要装箱。
需要装箱的历史 C# 结构的非详尽列表,您应该避免:
事件系统 天真的使用它时发现有一个竞争条件,并且它不支持异步。再加上拳击问题,也许应该可以避免。 (例如,您可以将其替换为使用泛型的异步事件系统。)
旧的 Threading 和 Timer 模型在其参数上强制使用 Box,但已被 async/await 取代,后者更干净、更高效。
.Net 1.1 集合完全依赖于 Boxing,因为它们出现在泛型之前。这些仍然在 System.Collections 中存在。在任何新代码中,您应该使用 System.Collections.Generic 中的集合,其中 除了避免 Boxing 之外,还为您提供了更强的类型安全性。
您应该避免将值类型声明或传递为对象,除非您必须处理上述强制装箱的历史问题,并且您希望避免稍后当您知道它将被装箱时对其性能造成影响。
根据 Mikael 下面的建议:
这样做
而不是这个
更新
这个答案最初建议 Int32、Bool 等导致装箱,而实际上它们是值类型的简单别名。也就是说,.Net 具有 Bool、Int32、String 等类型,C# 将它们别名为 bool、int、string,没有任何功能差异。
The best way to understand this is to look at lower-level programming languages C# builds on.
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.
Dealing with pointers is hard. They're the cause of memory leaks, buffer overruns, and frustration. C# to the rescue.
At a higher level, C#, you don't need to think about pointers - the .Net framework (written in C++) thinks about these for you and presents them to you as References to Objects, and for performance, lets you store simpler values like bools, bytes and ints as Value Types. Underneath the hood, Objects and stuff that instantiates a Class go on the expensive, Memory-Managed Heap, while Value Types go in that same Stack you had in low-level C - super-fast.
For the sake of keeping the interaction between these 2 fundamentally different concepts of memory (and strategies for storage) simple from a coder's perspective, Value Types can be Boxed at any time. Boxing causes the value to be copied from the Stack, put in an Object, and placed on the Heap - more expensive, but, fluid interaction with the Reference world. As other answers point out, this will occur when you for example say:
A strong illustration of the advantage of Boxing is a check for null:
Our object o is technically an address in the Stack that points to a copy of our bool b, which has been copied to the Heap. We can check o for null because the bool's been Boxed and put there.
In general you should avoid Boxing unless you need it, for example to pass an int/bool/whatever as an object to an argument. There are some basic structures in .Net that still demand passing Value Types as object (and so require Boxing), but for the most part you should never need to Box.
A non-exhaustive list of historical C# structures that require Boxing, that you should avoid:
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.
You should avoid declaring or passing your Value Types as objects, unless you have to deal with the above historical problems that force Boxing, and you want to avoid the performance hit of Boxing it later when you know it's going to be Boxed anyway.
Per Mikael's suggestion below:
Do This
Not This
Update
This answer originally suggested Int32, Bool etc cause boxing, when in fact they are simple aliases for Value Types. That is, .Net has types like Bool, Int32, String, and C# aliases them to bool, int, string, without any functional difference.
装箱并不是您真正使用的东西 - 它是运行时使用的东西,以便您可以在必要时以相同的方式处理引用和值类型。例如,如果您使用 ArrayList 来保存整数列表,则整数会被装箱以适合 ArrayList 中的对象类型槽。
现在使用通用集合,这个问题几乎消失了。如果您创建
List
,则不会进行装箱 -List
可以直接保存整数。Boxing isn't really something that you use - it is something the runtime uses so that you can handle reference and value types in the same way when necessary. For example, if you used an ArrayList to hold a list of integers, the integers got boxed to fit in the object-type slots in the ArrayList.
Using generic collections now, this pretty much goes away. If you create a
List<int>
, there is no boxing done - theList<int>
can hold the integers directly.装箱和拆箱专门用于将值类型对象视为引用类型;将它们的实际值移动到托管堆并通过引用访问它们的值。
如果没有装箱和拆箱,你永远无法通过引用传递值类型;这意味着您无法将值类型作为对象的实例传递。
Boxing and Unboxing are specifically used to treat value-type objects as reference-type; moving their actual value to the managed heap and accessing their value by reference.
Without boxing and unboxing you could never pass value-types by reference; and that means you could not pass value-types as instances of Object.
我最后一次必须拆箱的地方是编写一些从数据库检索一些数据的代码时(我没有使用 LINQ 到 SQL,只是简单的旧 ADO.NET ):
基本上,如果您在泛型之前使用较旧的 API,您会遇到装箱问题。除此之外,这种情况并不常见。
The last place I had to unbox something was when writing some code that retrieved some data from a database (I wasn't using LINQ to SQL, just plain old ADO.NET):
Basically, if you're working with older APIs before generics, you'll encounter boxing. Other than that, it isn't that common.