C#中的装箱事件

Posted

技术标签:

【中文标题】C#中的装箱事件【英文标题】:Boxing Occurrence in C# 【发布时间】:2011-12-21 04:33:18 【问题描述】:

我正在尝试收集 C# 中发生拳击的所有情况:

将值类型转换为System.Object 类型:

struct S  
object box = new S();

将值类型转换为System.ValueType 类型:

struct S  
System.ValueType box = new S();

将枚举类型的值转换为System.Enum类型:

enum E  A 
System.Enum box = E.A;

将值类型转换为接口引用:

interface I  
struct S : I  
I box = new S();

在 C# 字符串连接中使用值类型:

char c = F();
string s1 = "char value will box" + c;

注意:char 类型的常量在编译时连接

注意:自 6.0 版以来 C# 编译器 optimizes concatenation 涉及 boolcharIntPtrUIntPtr 类型

从值类型实例方法创建委托:

struct S  public void M()  
Action box = new S().M;

在值类型上调用非覆盖的虚方法:

enum E  A 
E.A.GetHashCode();

is 表达式下使用C# 7.0 常量模式:

int x = …;
if (x is 42)  …  // boxes both 'x' and '42'!

C# 元组类型转换中的装箱:

(int, byte) _tuple;

public (object, object) M() 
  return _tuple; // 2x boxing

object类型带值类型默认值的可选参数:

void M([Optional, DefaultParameterValue(42)] object o);
M(); // boxing at call-site

检查null 的无约束泛型类型的值:

bool M<T>(T t) => t != null;
string M<T>(T t) => t?.ToString(); // ?. checks for null
M(42);

注意:这可能会在某些 .NET 运行时通过 JIT 进行优化

使用is/as 运算符对无约束或struct 泛型类型进行类型测试:

bool M<T>(T t) => t is int;
int? M<T>(T t) => t as int?;
IEquatable<T> M<T>(T t) => t as IEquatable<T>;
M(42);

注意:这可能会在某些 .NET 运行时通过 JIT 进行优化

还有其他你知道的拳击情况吗,也许是隐藏的?

【问题讨论】:

我前段时间处理过这个问题,发现这很有趣:Detecting (un)boxing using FxCop 应该是社区wiki问题 可空类型呢? private int? nullableInteger @allansson,可空类型只是一种值类型 请注意,从 .NET Core 2.1 开始,Enum.HasFlag 不会装箱,我相信:blogs.msdn.microsoft.com/dotnet/2018/04/18/…。虽然我可以在 2.1 应用程序中看到 IL 中的 box 指令,但它没有分配,因此我没有看到性能损失。 【参考方案1】:

这是一个很好的问题!

装箱的发生只有一个原因:当我们需要一个值类型的引用时。您列出的所有内容都属于此规则。

例如,由于 object 是引用类型,将值类型转换为 object 需要对值类型的引用,这会导致装箱。

如果您希望列出所有可能的场景,您还应该包括派生类,例如从返回对象或接口类型的方法中返回值类型,因为这会自动将值类型转换为对象/接口。

顺便说一句,您敏锐地识别出的字符串连接情况也源自强制转换为对象。 + 运算符由编译器转换为对字符串的 Concat 方法的调用,该方法接受您传递的值类型的对象,因此转换为对象并因此发生装箱。

多年来,我一直建议开发人员记住装箱的单一原因(我在上面指定),而不是记住每一个案例,因为列表很长而且很难记住。这也促进了对编译器为我们的 C# 代码生成的 IL 代码的理解(例如 + on string 产生对 String.Concat 的调用)。当您对编译器生成的内容有疑问并且发生装箱时,您可以使用 IL Disassembler (ILDASM.exe)。通常,您应该查找框操作码(即使 IL 不包含框操作码,只有一种情况可能会发生装箱,下面会详细说明)。

但我确实同意某些拳击事件不太明显。您列出了其中之一:调用值类型的非覆盖方法。事实上,这不太明显还有一个原因:当你检查 IL 代码时,你看不到框操作码,而是约束操作码,所以即使在 IL 中,装箱也不明显发生!我不会详细说明为什么要阻止这个答案变得更长......

另一种不太明显的装箱情况是从结构调用基类方法时。示例:

struct MyValType

    public override string ToString()
    
        return base.ToString();
    

这里 ToString 被覆盖,因此在 MyValType 上调用 ToString 不会生成装箱。但是,该实现调用了基础 ToString,这会导致装箱(检查 IL!)。

顺便说一句,这两个不明显的拳击场景也源自上面的单一规则。当在值类型的基类上调用方法时,this 关键字必须引用某些内容。由于值类型的基类(总是)是引用类型,this 关键字必须引用引用类型,因此我们需要引用值类型,因此由于单一规则。

这是我的在线 .NET 课程中详细讨论拳击的部分的直接链接:http://motti.me/mq

如果您只对更高级的拳击场景感兴趣,这里有一个直接链接(尽管上面的链接会在讨论更基本的内容后将您带到那里):http://motti.me/mu

我希望这会有所帮助!

莫蒂

【讨论】:

如果 ToString() 被调用的特定值类型没有覆盖它,该值类型会在调用站点被装箱,还是将方法分派(不装箱)到自动生成的覆盖,除了链接(使用装箱)到基本方法之外什么都不做? @supercat 调用任何在值类型中调用base 的方法都会导致装箱。这包括未被结构覆盖的虚拟方法和根本不是虚拟的Object 方法(如GetType())。见this question。 @ŞafakGür:我知道最终的结果将是拳击。我想知道它发生的确切机制。由于生成 IL 的编译器可能不知道该类型是值类型还是引用(它可能是通用的),所以无论如何它都会生成一个 callvirt。 JITter 会知道该类型是否是值类型,以及它是否覆盖 ToString,因此它可以生成调用站点代码来进行装箱;或者,它可以为每个不覆盖 ToString 的结构自动生成一个方法 public override void ToString() return base.ToString(); 和... ...在该方法中进行装箱。由于该方法很短,因此可以内联。使用后一种方法做事将允许通过反射访问结构的 ToString() 方法,就像其他任何方法一样,并用于创建一个静态委托,该委托将结构类型作为 ref 参数[这样的事情适用于非继承struct methods],但我只是尝试创建一个这样的委托,但它没有用。是否可以为结构的ToString() 方法创建静态委托,如果可以,参数类型应该是什么? 链接已损坏。【参考方案2】:

在值类型上调用非虚拟 GetType() 方法:

struct S  ;
S s = new S();
s.GetType();

【讨论】:

GetType 需要装箱,不仅因为它是非虚拟的,还因为值类型的存储位置,不像堆对象,没有 GetType() 可以用来识别的“隐藏”字段他们的类型。 @supercat 嗯。 1. 编译器添加的装箱和运行时使用的隐藏字段。可能是编译器添加了装箱,因为它知道运行时...... 2. 当我们在枚举值上调用非虚拟 ToString(string) 时,它也需要装箱,我不相信编译器会添加这个,因为它知道 Enum.ToString(string) 实现细节.因此,我认为,我可以说,当调用“基值类型”上的非虚拟方法时,总是会发生装箱。 我没有考虑过Enum 有任何自己的非虚拟方法,尽管EnumToString() 方法需要访问类型信息。我想知道ObjectValueTypeEnum 是否有任何非虚拟方法可以在没有类型信息的情况下执行其工作。【参考方案3】:

在 Motti 的回答中提到,只是用代码示例说明:

涉及的参数

public void Bla(object obj)




Bla(valueType)

public void Bla(IBla i) //where IBla is interface




Bla(valueType)

但这是安全的:

public void Bla<T>(T obj) where T : IBla




Bla(valueType)

返回类型

public object Bla()

    return 1;


public IBla Bla() //IBla is an interface that 1 inherits

    return 1;

检查不受约束的 T 与 null

public void Bla<T>(T obj)

    if (obj == null) //boxes.

动态的使用

dynamic x = 42; (boxes)

另一个

enumValue.HasFlag

【讨论】:

【参考方案4】: 使用System.Collections 中的非泛型集合,例如 ArrayListHashTable

当然,这些是您的第一个案例的具体实例,但它们可能是隐藏的陷阱。令人惊讶的是,我今天仍然遇到大量使用这些而不是 List&lt;T&gt;Dictionary&lt;TKey,TValue&gt; 的代码。

【讨论】:

【参考方案5】:

将任何值类型的值添加到 ArrayList 中会导致装箱:

ArrayList items = ...
numbers.Add(1); // boxing to object

【讨论】:

以上是关于C#中的装箱事件的主要内容,如果未能解决你的问题,请参考以下文章

C# 中的 var 关键字会导致装箱吗?

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

Java中的值类型

c#中的泛型

了解 C# 中的事件和事件处理程序

C# 中的委托和事件