为什么代表引用类型?

时间:2011-10-26 16:43:34

标签: c# .net delegates value-type reference-type

关于已接受答案的快速说明:我不同意Jeffrey's answer的一小部分内容,即由于Delegate必须是参考类型,因此遵循所有代表都是引用类型。 (多层继承链排除值类型并不是真的;例如,所有枚举类型都继承自System.EnumSystem.ValueType继承自System.Object,继承自{{ 1}},所有引用类型。)但是,我认为事实上,从根本上说,所有代表事实上不仅仅来自Delegate而是来自MulticastDelegate,这是关键的实现。作为对他的答案的评论中的Raymond points out,一旦您承诺支持多个订阅者,使用代理的引用类型就没有意义鉴于某个地方需要一个阵列,它本身就是一个。


请参阅底部的更新。

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

Action foo = obj.Foo;

我每次都在创建一个新的 Action对象。我确信成本很低,但它涉及分配内存以便以后进行垃圾回收。

鉴于代理本质上自己不可变,我想知道为什么它们不能成为值类型?然后像上面那样的一行代码只会对堆栈上的内存地址进行简单的赋值*。

即使考虑匿名函数,似乎( me )这也行。请考虑以下简单示例。

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

在这种情况下,foo确实构成闭包,是的。在许多情况下,我想这确实需要一个实际的引用类型(例如当局部变量被关闭并在闭包内被修改时)。 但在某些情况下,它不应该。例如,在上面的例子中,似乎支持闭包的类型可能如下所示: 我收回了关于此的原始观点。下面确实需要是一个引用类型(或者:它不需要需要,但是如果它是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;

这个问题有意义吗?在我看来,有两种可能的解释:

  • 作为值类型正确实现委托需要额外的工作/复杂性,因为对 修改局部变量值的闭包这样的东西的支持无论如何都需要编译器生成的引用类型。
  • 还有一些其他原因,为什么,代理人只是无法实现为值类型。

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


更新:为了回应Ani的评论,我明白为什么上面示例中的CompilerGenerated类型也可能是引用类型,因为如果委托将包含一个函数指针和一个对象指针,无论如何都需要一个引用类型(至少对于使用闭包的匿名函数,因为即使你引入了一个额外的泛型类型参数 - 例如,Action<TCaller> - 这也不会涵盖那些可能的类型'被命名!)。 然而,所有这一切都让我后悔让闭包将编译器生成的闭包问题带入讨论中!我的主要问题是委托,即带有函数指针和对象指针的。在我看来, 可能是一种值类型。

换句话说,即使这......

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

...需要创建一个引用类型对象(以支持闭包,并为委托提供引用),为什么需要创建两个(封闭支持对象加上 Action委托)?

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

7 个答案:

答案 0 :(得分:16)

问题归结为:CLI(公共语言基础结构)规范说委托是引用类型。为什么会这样?

今天在.NET Framework中可以清楚地看到一个原因。在原始设计中,有两种委托:普通委托和“多播”委托,它们的调用列表中可以有多个目标。 MulticastDelegate类继承自Delegate。由于您无法从值类型继承,因此Delegate必须是引用类型。

最后,所有实际的委托最终成为多播委托,但在此过程中,合并这两个类为时已晚。有关此确切主题,请参阅此blog post

  

我们放弃了Delegate和MulticastDelegate之间的区别   在V1结束时。那时,它本来是一个大规模   更改为合并这两个类,所以我们没有这样做。你应该   假装它们已合并,并且只存在MulticastDelegate。

此外,代表们目前有4-6个字段,都是指针。 16字节通常被认为是上限,其中保存内存仍然胜过额外复制。 64位MulticastDelegate占用48个字节。鉴于此,以及他们使用继承这一事实表明一个班级是自然的选择。

答案 1 :(得分:9)

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

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

答案 2 :(得分:7)

想象一下,如果代表是值类型。

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?

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

struct CompilerGenerated
{
    int counter = 0;
    public Execute() { ++counter; }
};

Notify handler = new CompilerGenerated();
SignalTwice(handler);
System.Console.WriteLine(counter); // what should this print?

如果delegate是值类型,则SignalEvent会获得handler的副本,这意味着将创建一个全新的CompilerGeneratedhandler的副本{1}})并传递给SignalEventSignalTwice将执行两次委托,这会使副本中的counter两次增加。然后SignalTwice返回,函数打印0,因为原始文件未被修改。

答案 3 :(得分:4)

这是一个不知情的猜测:

如果将委托实现为值类型,则复制实例的成本非常高,因为委托实例相对较重。也许MS认为将它们设计为不可变的引用类型会更安全 - 复制机器字大小的实例引用相对便宜。

委托实例至少需要:

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

让我们假设value-type委托以与当前引用类型实现类似的方式实现(这可能有点不合理;可能已经选择了不同的设计来保持大小不变​​)来说明。使用Reflector,以下是委托实例中所需的字段:

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

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


另一方面,我想问一下,在你提出的设计中,如何使CompilerGenerated闭包类型的结构以任何方式帮助。创建的委托的对象指针指向哪里?如果没有适当的逃逸分析,将闭包类型实例留在堆栈上将是非常风险的业务。

答案 4 :(得分:2)

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

  

不可变并不意味着它必须是值类型。还有一些东西   值类型不需要是不可变的。这两个经常去   手拉手,但实际上并不是同一件事,而且有   实际上是.NET Framework中每个的反例(String   例如,课程。

答案是:

  

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

取自here

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

答案 5 :(得分:2)

我可以说,将代理作为引用类型绝对是一个糟糕的设计选择。它们可以是值类型,仍然支持多播代理。

想象一下,Delegate是一个由以下组成的结构,让我们说: 对象目标; 指向方法的指针

它可以是一个结构,对吗? 只有当目标是结构时才会发生装箱(但委托本身不会被装箱)。

您可能认为它不支持MultiCastDelegate,但我们可以: 创建一个包含普通委托数组的新对象。 将一个Delegate(作为结构)返回给该新对象,该对象将实现Invoke迭代其所有值并在其上调用Invoke。

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


作为旁注,方差不要求Delegate是引用类型。委托的参数应该是引用类型。毕竟,如果传递一个字符串,则需要一个对象(对于输入,而不是ref或out),则不需要强制转换,因为字符串已经是对象。

答案 6 :(得分:1)

我猜一个原因是支持多播委托多播委托比简单的几个字段表示目标和方法更复杂。

这种形式唯一可能的是委托方差。这种差异需要两种类型之间的参考转换。

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