C#中的拳击事件


85

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

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

    struct S { }
    object box = new S();
    
  • 将值类型转换为System.ValueType类型:

    struct S { }
    System.ValueType box = new S();
    
  • 将枚举类型的值转换为System.Enumtype:

    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#编译器提供更优的级联涉及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可能对此进行了优化

  • struct使用is/as运算符的无约束或通用类型的类型测试值:

    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可能对此进行了优化

您是否知道还有更多拳击的情况,也许是隐藏的?


2
我前一段时间处理了这个问题,发现这很有趣:使用FxCop检测(取消)装箱
George Duckett

您显示的转换方式非常好。实际上,我不知道第二个,或者也许从未尝试过它:)谢谢
Zenwalker 2011年

12
它应该是社区Wiki问题
Sly

2
那可空类型呢? private int? nullableInteger
allansson 2011年

1
@allansson,可为空的类型只是值类型的一种
controlflow

Answers:


42

这是一个很好的问题!

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

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

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

顺便说一句,您敏锐地确定的字符串连接大小写情况也源自转换为对象。编译器将+运算符转换为对String的Concat方法的调用,该方法接受您所传递的值类型的对象,因此将其强制转换为对象,从而发生装箱。

多年来,我一直建议开发人员记住装箱的唯一原因(我在上面指定),而不是记住每个案例,因为该列表很长且很难记住。这也有助于理解编译器为我们的C#代码生成的IL代码(例如,字符串上的+产生对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

我希望这有帮助!

莫蒂


1
如果ToString()在没有覆盖它的特定值类型上调用了a,则该值类型将在调用站点处装箱,还是将方法(不装箱)分派到自动生成的覆盖中,该覆盖项只做链结(拳击)的基本方法?
supercat 2014年

@supercat调用任何base以值类型调用的方法都将导致装箱。这包括未被struct覆盖的虚拟方法和Object根本不是虚拟的方法(例如GetType())。看到这个问题
Şafak古尔

@ŞafakGür:我知道最终结果将是拳击。我想知道它发生的确切机制。由于生成IL的编译器可能不知道类型是值类型还是引用(可能是通用的),因此无论如何都会生成callvirt。JITter会知道该类型是否为值类型,以及它是否覆盖ToString,因此它可以生成调用站点代码以进行装箱。或者,它可以为不覆盖ToString方法的每个结构自动生成public override void ToString() { return base.ToString(); }...
supercat

...拳击在该方法内发生。由于该方法非常短,因此可以内联。使用后一种方法做事将允许ToString()通过反射像其他方法一样访问结构的方法,并用于创建将结构类型作为ref参数的静态委托[这种事情适用于非继承的结构方法],但是我只是尝试创建这样的委托,但是没有用。是否可以为结构的ToString()方法创建静态委托,如果可以,则参数类型应该是什么?
supercat

链接断开。
OfirD

5

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

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

2
GetType需要装箱不仅是因为它不是虚拟的,而且因为值类型的存储位置与堆对象不同,没有“隐藏”字段GetType()可用于标识其类型。
supercat 2014年

@supercat嗯。1.编译器添加的装箱和运行时使用的隐藏字段。可能是编译器添加拳击,因为它知道运行时…2.当我们在枚举值上调用非虚拟ToString(string)时,它也需要装箱,我不相信编译器添加它,因为它知道Enum.ToString(string)的实现细节。因此,我认为可以说,装箱总是在调用“基值类型”的非虚拟方法时发生。
Viacheslav Ivanov

我没有考虑过Enum拥有任何非虚拟方法,尽管ToString()for的方法Enum需要访问类型信息。我不知道ObjectValueTypeEnum有可能没有类型的信息进行自己的工作任何非虚方法。
2014年

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


0
  • 在使用非泛型集合System.Collections,如 ArrayListHashTable

当然,这些是您第一种情况的特定实例,但它们可能是隐藏的陷阱。我今天仍然遇到使用这些代码代替List<T>and的大量代码,这令人惊讶Dictionary<TKey,TValue>


0

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

ArrayList items = ...
numbers.Add(1); // boxing to object
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.