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

Vai*_*ain 307 .net c# boxing

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

我知道拳击和拆箱是什么,但我无法理解它的实际用途.我应该在哪里以及在哪里使用它?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing
Run Code Online (Sandbox Code Playgroud)

jas*_*son 462

为什么

拥有统一的类型系统并允许值类型从引用类型表示其基础数据的方式中获得完全不同的基础数据表示(例如,a int只是一个32位的桶,与参考完全不同类型).

想想这样.你有一个o类型的变量object.现在你有一个int,你想要把它o.o是某个地方的某个东西的引用,并且int强调不是某个地方的某个东西的引用(毕竟,它只是一个数字).所以,你做的是:你创建一个object可以存储的新东西,int然后你将该对象的引用分配给o.我们称这个过程为"拳击".

所以,如果你不关心拥有统一的类型系统(即,引用类型和值类型具有非常不同的表示,并且你不想要一种"代表"两者的常用方法),那么你不需要装箱.如果你不关心int代表它们的基础价值(即,也int有引用类型,只是存储对它们的基础值的引用),那么你不需要装箱.

我应该在哪里使用它.

例如,旧的集合类型ArrayList只吃objects.也就是说,它只存储对某些地方的某些东西的引用.没有拳击,你不能把这个int集合.但是对于拳击,你可以.

现在,在仿制药的时代,你真的不需要这个,并且通常可以快乐地走,而不考虑问题.但有一些需要注意的注意事项:

这是对的:

double e = 2.718281828459045;
int ee = (int)e;
Run Code Online (Sandbox Code Playgroud)

这不是:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)o; // runtime exception
Run Code Online (Sandbox Code Playgroud)

相反,你必须这样做:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)(double)o;
Run Code Online (Sandbox Code Playgroud)

首先,我们必须显式地取消装箱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);
Run Code Online (Sandbox Code Playgroud)

在继续下一句话之前,请考虑一下.

如果你说的TrueFalse伟大!等等,什么?那是因为==引用类型使用引用相等性来检查引用是否相等,而不是基础值是否相等.这是一个非常容易犯的错误.也许更微妙

double e = 2.718281828459045;
object o1 = e;
object o2 = e;
Console.WriteLine(o1 == o2);
Run Code Online (Sandbox Code Playgroud)

还会打印False!

更好地说:

Console.WriteLine(o1.Equals(o2));
Run Code Online (Sandbox Code Playgroud)

然后,谢天谢地,打印出来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);
Run Code Online (Sandbox Code Playgroud)

什么是输出?这取决于!如果Point是a struct然后输出是,1但如果Point是a class然后输出是2!装箱转换使得装箱的值的副本解释了行为的差异.

  • 你能否谈谈"拳击"和"拆箱"的性能影响? (3认同)
  • 出色的答案-比我在知名书籍中阅读的大多数解释要好。 (2认同)

小智 53

在.NET框架中,有两种类型 - 值类型和引用类型.这在OO语言中相对常见.

面向对象语言的一个重要特性是能够以类型无关的方式处理实例.这被称为多态性.既然我们想利用多态,但我们有两种不同的类型,必须有一些方法将它们组合在一起,这样我们才能以相同的方式处理其中一种.

现在,回到过去的日子(Microsoft.NET的1.0),没有这个新奇的泛型hullabaloo.您无法编写具有可以为值类型和引用类型提供服务的单个参数的方法.这是对多态性的违反.因此采用拳击作为将值类型强制转换为对象的手段.

如果这是不可能的,那么框架将充满方法和类,其唯一目的是接受其他类型的类型.不仅如此,但由于值类型并不真正共享一个共同类型的祖先,因此每个值类型(bit,byte,int16,int32等等)都必须有不同的方法重载.

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


Chr*_*ini 34

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

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

在下一级,像C++一样,引入了第二个称为Heap的内存结构.你仍然主要生活在Stack中,但是可以将特殊的名为Pointers的内容添加到Stack中,它存储Object的第一个字节的内存地址,并且该Object存在于堆中.堆是一种混乱,维护起来有些昂贵,因为与堆栈变量不同,它们不会在程序执行时线性上升然后下降.它们可以不按顺序进出,它们可以生长和缩小.

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

在更高级别的C#中,您不需要考虑指针 - .Net框架(用C++编写)为您考虑这些并将它们作为对象的引用呈现给您,并且为了提高性能,可以存储更简单的值像bools,bytes和int作为Value Types.在底层,对象和实例化一个类的东西依赖于昂贵的内存管理堆,而值类型进入你在低级别C中的那个堆栈 - 超快速.

为了保持这两个根本不同的内存概念(和存储策略)之间的相互作用,从编码器的角度来看,可以随时使用值类型.拳击使得值从堆栈中复制,放入一个对象,并放置在堆上 - 更昂贵,但与参考世界的流畅交互.正如其他答案所指出的那样,当你举例说:

bool b = false; // Cheap, on Stack
object o = b; // Legal, easy to code, but complex - Boxing!
bool b2 = (bool)o; // Unboxing!
Run Code Online (Sandbox Code Playgroud)

拳击优势的一个有力例证是检查null:

if (b == null) // Will not compile - bools can't be null
if (o == null) // Will compile and always return false
Run Code Online (Sandbox Code Playgroud)

我们的对象o在技术上是Stack中的一个地址,指向我们的bool b的副本,该副本已被复制到堆中.我们可以检查o为null,因为bool已被装箱并放在那里.

通常你应该避免使用Boxing,除非你需要它,例如将int/bool/whatever作为对象传递给参数..Net中有一些基本结构仍然要求将值类型作为对象传递(因此需要使用Boxing),但在大多数情况下,您永远不需要Box.

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

  • 事件系统在天真地使用它时会产生竞争条件,并且它不支持异步.加入拳击问题,应该避免它.(您可以将其替换为使用泛型的异步事件系统.)

  • 旧的线程和计时器模型强制一个Box的参数,但已被async/await取代,它们更清晰,更高效.

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

你应该避免声明或传递你的价值类型作为对象,除非你必须处理强迫拳击的上述历史问题,并且你想要避免以后当你知道它将被装箱时击中它的性能.

根据Mikael的建议如下:

做这个

using System.Collections.Generic;

var employeeCount = 5;
var list = new List<int>(10);
Run Code Online (Sandbox Code Playgroud)

不是这个

using System.Collections;

Int32 employeeCount = 5;
var list = new ArrayList(10);
Run Code Online (Sandbox Code Playgroud)

更新

这个答案最初建议Int32,Bool等导致拳击,实际上它们是值类型的简单别名.也就是说,.Net有像Bool,Int32,String和C#这样的类型将它们别名为bool,int,string,没有任何功能差异.

  • 你教会了我一百多名程序员和IT专业人员多年来无法解释的事情,但是改变它来说出你应该做什么而不是要避免什么,因为它有点难以理解......基本规则最常见的不是1你不应该这样做,而是这样做 (3认同)
  • c#中没有"Int",有int和Int32.我认为说明一个是值类型而另一个是包含值类型的引用类型是错误的.除非我弄错了,在Java中也是如此,但不是C#.在C#中,在IDE中显示蓝色的那些是其结构定义的别名.所以:int = Int32,bool = Boolean,string = String.使用bool over Boolean的原因是因为在MSDN设计指南和约定中建议使用bool.否则我喜欢这个答案.但我会投票,直到你证明我错了或在你的答案中解决这个问题. (3认同)
  • 这个答案应该被标记为ANSWER一百次了! (2认同)
  • 如果将变量声明为int,并将另一个变量声明为Int32,或者将bool和Boolean声明为-右键单击并查看定义,则最终将得到结构的相同定义。 (2认同)
  • @HeribertoLugo是正确的,"你应该避免将你的价值类型宣布为Bool而不是bool"是错误的.正如OP指出的那样,你应该避免将你的bool(或布尔值或任何其他值类型)声明为Object.bool/Boolean,int/Int32,只是C#和.NET之间的别名:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/built-in-types-table (2认同)
  • “在下一个级别,如 C++,引入了称为堆的第二个内存结构”......我认为这是不正确的。C 还使用 malloc 而不是 new 在堆上分配?我认为 C++ 中没有引入堆。 (2认同)
  • 我试图在这里快速总结很多内容;是的,确实可以用 C 语言构建更高级结构的基本基础,包括使用 malloc 访问堆 - 并且管理它很快就会变得混乱。但是你永远不会像在 C++ 和 C# 中那样隐式地将东西转储到那里,甚至一些简单的 C# 与堆的交互在 C 中也会变得非常复杂。这个答案的目标是让刚开始使用 C# 的人了解什么拳击是什么以及为什么你会拥有它,而无需提供计算机科学的详尽历史或 C 或 C++ 或其他语言的研究 (2认同)
  • 这应该是一个选择的答案!很好的解释了为什么。 (2认同)

Ray*_*Ray 21

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

现在使用通用集合,这几乎消失了.如果你创建了一个List<int>,没有完成拳击 - List<int>可以直接保存整数.


STW*_*STW 12

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

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

  • 数字类型的引用传递存在于没有装箱的语言中,其他语言实现将值类型视为对象的实例,而无需装箱并将值移动到堆(例如,指针与 4 字节边界对齐的动态语言的实现使用较低的 4 个字节)引用位来指示该值是整数或符号而不是完整对象;此类值类型是不可变的并且大小与指针相同)。 (2认同)

BFr*_*ree 7

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

int myIntValue = (int)reader["MyIntValue"];
Run Code Online (Sandbox Code Playgroud)

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