3

简而言之,我认为拳击是一种烦恼。让我们看看一些替代方案......

public class Box<T> 
    where T : struct
{
    public T Value { get; set; }

    public static implicit operator T(Box<T> box)
    {
        return box.Value;
    }
}

System.Int32 派生自抽象类 System.ValueType,后者派生自 System.Object 类。您不能从 C# 中的 System.ValueType 派生,但我猜想 struct 关键字正是这样做的,并且 CLI 将这些类型定义识别为具有按值传递的语义。无论如何,当将结构分配给对象类型时,就会发生装箱。我不想陷入拳击本身,而是想直接进入它。

我查看了一些由 C# 编译器生成的 IL。

object obj = 1;

.locals init ([0] object obj)
L_0000: nop 
L_0001: ldc.i4.1 
L_0002: box int32 // Convert a value type (of the type specified in valTypeToken) to a true object reference. 
L_0007: stloc.0 

在 MSDN 上找到这个...

值类型在公共语言基础结构 (CLI) 中有两种不同的表示形式:

  • 当值类型嵌入另一个对象或堆栈时使用的“原始”形式。

  • 一种“盒装”形式,其中值类型中的数据被包装(盒装)到一个对象中,因此它可以作为独立实体存在。

这使我得出结论,编写这样的代码应该同样昂贵......

var box = obj as Box<int>;
if (box != null)
{
    Console.WriteLine(box.Value);
}

如果我打算将相同的值作为 System.Object 传递,我真的想每次都将 ValueType 拆箱和装箱吗?我的直觉告诉我不,但我真的找不到任何人愿意评论所有这些喋喋不休的好动机?

编辑

有人发现自己在这样做吗?我意识到它可能看起来很奇怪,但有一次我发现自己处于一个位置,我想基于几种不同的表示来抽象计算。我是这样做的,并使用 lambda 表达式。它与装箱并没有真正的关系,但它允许我将任何 ValueType(这个结构方便地以 8 字节对齐)视为一种单一类型“ReinterpretCast”。

[StructLayout(LayoutKind.Explicit)]
public struct ReinterpretCast
{
    [FieldOffset(0)] sbyte @sbyte;
    [FieldOffset(0)] byte @byte;
    [FieldOffset(0)] short @ushort;
    [FieldOffset(0)] ushort @short;
    [FieldOffset(0)] int @int;
    [FieldOffset(0)] uint @uint;
    [FieldOffset(0)] long @long;
    [FieldOffset(0)] ulong @ulong;
    [FieldOffset(0)] float @float;
    [FieldOffset(0)] double @double;
}
4

5 回答 5

2

我不完全确定你的问题。您只是在问您的解决方案是否比普通拳击更好?它当然有一些吸引力。如果您要问为什么一开始没有以这种方式实现装箱,请记住 .NET 没有泛型开始。

编辑:无论如何,拳击对于泛型来说是相对罕见的。不要忘记,如果对您的类型的实例的引用被传递为 as object(这通常是装箱的情况),您仍然需要进行运行时强制转换。也不要忘记接口——如果一个值类型实现了一个接口,那么它对应的用于装箱的引用类型也是如此。您的解决方案不会删除拳击的使用,因为您不能让您的类型“假装”来实现接口。(你也许可以用 DLR 做点什么,但到那时大部分要点都已经丢失了 :)

于 2009-02-10T17:49:34.667 回答
2

我们认为更快的是完全无关紧要的。在考虑什么更快时,只有探查器是相关的。

于 2009-02-10T17:52:35.050 回答
1

“如果我打算将相同的值作为对象传递,我真的想每次都拆箱/装箱吗?”

简短的回答:不,你不想做很多装箱/拆箱。它会产生开销:额外的额外垃圾并且往往很慢(尽管我认为速度已经在以后的框架版本中进行了优化)。

编辑:但是,如果您“将相同的值作为对象传递”,则在需要之前不强制转换为值类型,那么它会一直装箱而不会被拆箱。

但是,正如每个人所说,无论如何您都不需要“将相同的值作为对象传递”。这就是泛型的用途,除非您正在使用 Framework 1.x。那时,当 BCL 集合类使用 System.Object 并且任何进入的值类型都被装箱时,装箱更加相关。

(顺便说一句,如果通过接口访问装箱的值类型,则不会取消装箱。)

于 2009-02-10T19:17:09.013 回答
1

您的问题的标题错过了我认为最有趣的方面:​​系统的拳击行为与Box<T>类型的行为有何不同。有几点不同:

(1) boxedT将实现与 a 相同的接口T,使用相同的代码,但主要使用类语义而不是值语义,但使用古怪的Equals方法。

(2)T在 C# 或 vb.net 中,改变一个装箱通常会很麻烦,但是一个装箱T永远不会真正不可变,因为允许不受信任的可验证代码这样做,即使它在某些语言中很尴尬(在 C++/CLI 中很容易) . 甚至像 boxed 这样的类型在装箱Int32时也是可变的。相比之下,可以定义一个ImmutableBox<T>,它采用 type 的构造函数T,其字段将是真正不可变的。

(3) 即使是在装箱时很容易可变的结构类型(例如,因为它们实现了一个IEnumerator<T>Equals通常用它来测试它们的临时状态的相等性。相比之下,如果存在可变和不可变的盒子类型,则不可变类型可以检查状态是否相等,而可变类型可以检查引用是否相等。

(4) 隐式转换 from TtoBox<T>不会排除类型T定义隐式转换到接口类型的可能性。相比之下,因为所有类型都可以隐式转换为Object,所以 vb.net 和 C# 都不允许在结构类型和接口之间进行隐式用户转换。

(5) 如果没有专门的编译器对装箱的支持,目前接受 param-array of 的方法将Object[]无法自动将参数从诸如 to 之类的类型Int32转换Box<Int32>。另一方面,添加一种请求某些参数自动装箱的方法可能比在任何地方都使用隐式装箱更好。请注意,如果存在这样的方法,则可以指定每个参数都应放在 a 中Box<T>,从而可以区分传递 aT和 a Box<T>(因为后者将作为 a 传递Box<Box<T>>

于 2012-12-18T16:50:45.890 回答
0

好的,您在这里涉及到几个主题。首先,让我们看一下值类型以及它们存在的原因。当您需要值语义时,您可以使用值类型:

对于类,两个变量可以引用同一个对象,因此对一个变量的操作可能会影响另一个变量引用的对象。对于结构,每个变量都有自己的数据副本(除了 ref 和 out 参数变量),对一个变量的操作不可能影响另一个。此外,由于结构不是引用类型,因此结构类型的值不可能为空。

例如,所有数字类型都是值类型,因为它们需要具有值语义。如果变量x的值为 17 并且您将x分配给y,那么y将有自己的值 17 并且递增y不会将x更改为 18。因此,除非您有充分的理由,否则在定义类型时使用结构这需要具有价值语义。

在实现级别,通过使用内联分配来强制执行值语义。你可以在这里阅读更多关于它的信息。

这将我们引向拳击。拳击什么时候发生?当您将值类型转换为引用类型时。您使用类型对象作为示例,但对于 C# 泛型,这在实践中应该很少发生。更常见的情况是将值类型转换为接口。例如,将double类型转换为IEquatableIComparable。无论如何,如果您将值类型转换为引用类型,则装箱将并且必须发生。

当拳击发生时,真正发生了什么?制作要装箱的实例的副本并将其作为独立对象放置在堆上,以便即使原始实例超出范围时也可以安全地引用它。如果不是因为拳击,很容易让 CLR 尝试访问无效内存,我们都知道这是一件很糟糕的事情。

那么,拳击是好是坏呢?一方面它很好,因为它允许您在需要时安全地将值类型转换为引用类型。另一方面,它会创建“垃圾”——被丢弃并添加到垃圾收集器必须做的工作的对象的短暂实例。这很糟糕吗?仅在某些情况下,例如开发 XNA 游戏。如果这是你的情况,你会想要避免不受控制的拳击;如果是这样,我还邀请您访问我的博客,在那里我对该主题有一些建议

于 2009-02-10T18:16:22.903 回答