为什么代表参考types?

关于接受答案的快速注解 :我不同意Jeffrey答案的一小部分,即由于Delegate必须是参考types,因此所有代表都是参考types。 (事实上​​,多级inheritance链排除了值types是不正确的,例如,所有枚举types都从System.Enuminheritance,而System.Enuminheritance自inheritance自System.Object System.ValueType所有引用)然而,我认为从根本上说,所有代表实际上不仅仅inheritance自Delegate而且来自MulticastDelegate是这里的关键实现。 正如Raymond在他的回答中指出的那样 ,一旦你承诺支持多个订阅者,那么考虑到在某个地方需要一个数组,对于委托本身使用引用types是毫无意义的。


查看底部的更新。

我一直觉得奇怪,如果我这样做:

 Action foo = obj.Foo; 

我每次都创build一个新的 Action对象。 我相信这个代价是微不足道的,但是它涉及内存的分配,以便以后被垃圾收集。

鉴于代表本质上不变的,我想知道他们为什么不能成为价值types? 那么像上面那样的一行代码只会产生一个简单的分配给堆栈上的内存地址*。

即使考虑匿名function,似乎(对 )这将工作。 考虑下面的简单例子。

 Action foo = () => { obj.Foo(); }; 

在这种情况下, foo确实构成closures ,是的。 在很多情况下,我想这确实需要一个实际的引用types(例如当局部variables被closures并在闭包中被修改时)。 但在某些情况下,它不应该。 例如,在上面的例子中,似乎支持封闭的types可能是这样的: 我收回了我原来的观点。 下面的确需要成为一个引用types(或者: 不需要 ,但是如果它是一个struct它将会被装箱)。 所以,忽略下面的代码示例。 我留下它只提供回答的上下文特别提到它。

 struct CompilerGenerated { Obj obj; public CompilerGenerated(Obj obj) { this.obj = obj; } public void CallFoo() { obj.Foo(); } } // ...elsewhere... // This would not require any long-term memory allocation // if Action were a value type, since CompilerGenerated // is also a value type. Action foo = new CompilerGenerated(obj).CallFoo; 

这个问题有意义吗? 正如我所看到的,有两种可能的解释:

  • 作为值types正确地实现委托需要额外的工作/复杂性,因为支持像修改局部variables值的闭包这样的东西,无论如何都需要编译器生成的引用types。
  • 还有其他一些原因,为什么在代表中,代表根本无法实现值types。

最后,我不会为此而失眠, 这只是一段时间以来我一直很好奇的事情。


更新 :为了回应Ani的评论,我明白了为什么我上面的例子中的CompilerGeneratedtypes也可能是一个引用types,因为如果一个委托要包含一个函数指针和一个对象指针,它总是需要一个引用types至less对于使用闭包的匿名函数来说,因为即使你引入了一个额外的genericstypes参数,例如, Action<TCaller> – 这将不包括无法命名的types! 然而 ,所有这一切都让我感到遗憾,把编译器生成的closurestypes问题带入讨论中! 我的主要问题是关于代表 ,即函数指针和对象指针的东西。 在我看来,这仍然是一种价值types。

换句话说,即使这个…

 Action foo = () => { obj.Foo(); }; 

…需要创build一个引用types的对象(以支持闭包,并给委托的东西来引用),为什么它需要创build两个 (closures支持对象加上 Action委托)?

*是的,是的,实施细节,我知道! 我真正的意思是短期记忆储存

问题归结为:CLI(公共语言基础结构)规范说,代表是引用types。 这是为什么?

其中一个原因在.NET Framework中显而易见。 在原始devise中,有两种代表:普通代表和“多播”代表,它们的调用列表中可以有多个目标。 MulticastDelegate类inheritance自Delegate 。 由于不能从值typesinheritance,所以Delegate必须是引用types。

最后,所有实际的代表最终都是组播代表,但是在这个过程中,合并这两个类已经太晚了。 看到这个博客文章关于这个确切的话题:

我们在V1的末尾放弃了Delegate和MulticastDelegate之间的区别。 那个时候,把这两个class合并起来是一个很大的改变,所以我们没有这样做。 你应该假装他们被合并,只有MulticastDelegate存在。

另外,代表目前有4-6个字段,都是指针。 通常认为16字节是保存内存仍然胜过额外复制的上界。 一个64位的MulticastDelegate占用48个字节。 鉴于此,他们使用inheritance的事实表明,一个阶级是自然的select。

Delegate只有一个原因需要成为一个类,但这是一个很大的原因:委托可能足够小,以允许高效的存储作为值types(32位系统上的8个字节,或64位上的16个字节系统),它不可能小到足以保证一个线程试图写一个委托,而另一个线程试图执行它,后一个线程不会最终调用旧的方法在新的目标,或旧的目标的新方法。 容许这样的事情发生将是一个重大的安全漏洞。 让代表成为参考types避免了这种风险。

实际上,比代表结构types更好的是将它们作为接口。 创build闭包需要创build两个堆对象:一个编译器生成的对象来保存任何封闭的variables,以及一个委托来调用该对象的正确方法。 如果委托是接口,那么拥有封闭variables的对象本身可以用作委托,而不需要其他对象。

想象一下,如果代表是价值types。

 public delegate void Notify(); void SignalTwice(Notify notify) { notify(); notify(); } int counter = 0; Notify handler = () => { counter++; } SignalTwice(handler); System.Console.WriteLine(counter); // what should this print? 

根据您的build议,这将内部转换为

 struct CompilerGenerated { int counter = 0; public Execute() { ++counter; } }; Notify handler = new CompilerGenerated(); SignalTwice(handler); System.Console.WriteLine(counter); // what should this print? 

如果delegate是一个值types,那么SignalEvent将得到一个handler的副本,这意味着将会创build一个全新的CompilerGeneratedhandler副本)并传递给SignalEventSignalTwice会执行委托两次,这会增加副本中counter两次。 然后SignalTwice返回,并且该函数打印0,因为原件没有被修改。

这是一个不知情的猜测:

如果代表被实现为值types,那么由于委托实例相对较重,所以实例的复制将非常昂贵。 也许MS认为将它们devise为不可变的引用types会比较安全 – 将机器字大小的引用复制到实例相对便宜。

委托实例至less需要:

  • 对象引用(如果是实例方法,则为包装方法的“this”引用)。
  • 指向包装函数的指针。
  • 包含多播调用列表的对象的引用。 请注意,委托types应通过devise支持使用相同委托types的组播。

我们假设值types的委托是以类似于当前引用types实现的方式来实现的(这可能有点不合理;可能已经select了不同的devise来保持规模不变)来说明。 使用reflection器,这是委托实例中所需的字段:

 System.Delegate: _methodBase, _methodPtr, _methodPtrAux, _target System.MulticastDelegate: _invocationCount, _invocationList 

如果实现为一个结构(无对象头),这些将在x86上共计24个字节,在x64上共计48个字节,这对于一个结构来说是巨大的。


在另一个说明中,我想问一下,在你提出的devise中,如何使CompilerGenerated闭包types结构以任何方式提供帮助。 创build的委托对象指针指向哪里? 如果没有正确的逃逸分析,将闭包types实例留在堆栈上将是非常危险的业务。

我可以说,将代表作为参考types肯定是一个糟糕的deviseselect。 它们可以是价值types,并且仍然支持多播代表。

想象一下,Delegate是一个由以下组成的结构:let object:object object; 指向方法的指针

它可以是一个结构,对吗? 拳击只会发生如果目标是一个结构(但委托本身不会被装箱)。

你可能会认为它不会支持MultiCastDelegate,但是我们可以:创build一个新的对象来存放普通委托的数组。 将一个委托(作为结构)返回给该新对象,该对象将实现Invoke迭代其所有值并对其调用Invoke。

因此,对于普通的代表来说,这是永远不会调用两个或更多的处理程序,它可以作为一个结构。 不幸的是,这在.Net中是不会改变的。


作为一个方面的说明,差异不要求代表是引用types。 委托的参数应该是引用types。 毕竟,如果你传递一个string是一个对象是必需的(对于input,而不是ref或out),那么不需要强制转换,因为string已经是一个对象。

我在互联网上看到了这个有趣的对话:

不可变并不意味着它必须是一个值types。 而一些值types的东西并不是必须的。 这两者经常携手并进,但事实上并非如此,实际上.NET Framework中的每个例子(例如String类)都是相反的例子。

答案是:

不同之处在于,虽然不可变的引用types是相当普遍和完全合理的,但是使值types变化几乎总是一个坏主意,并且会导致一些非常混乱的行为!

从这里采取

所以,在我看来,这个决定是由语言可用性方面决定的,而不是编译器技术上的困难。 我喜欢可空的代表。

我想一个原因是支持多投代理多投代理比简单的指示目标和方法的几个字段更复杂。

另一件只有在这种forms下才有可能的是代表方差。 这种差异需要两种types之间的参考转换。

有趣的是F#定义了它自己的函数指针types,它与委托类似,但是更加轻量级。 但我不确定它是一个值还是参考types。