[英]Why are delegates reference types?
关于接受的答案的简要说明 :我不同意Jeffrey的答案的一小部分,即,由于Delegate
必须是引用类型,因此所有委托人都是引用类型。 (根本就不是多级继承链排除值类型;例如,所有枚举类型都继承自System.Enum
,而System.Enum
继承自System.ValueType
,后者继承自System.Object
, 所有引用但是,我认为,从根本上说,实际上所有委托都不仅从Delegate
继承,而且从MulticastDelegate
继承是这样的事实。 正如雷蒙德(Raymond)在对他的回答的评论中指出的那样 ,一旦您承诺支持多个订户,考虑到在某个地方需要一个数组,对于委托本身不使用引用类型真的没有意义。
请参阅底部的更新。
我一直觉得奇怪,如果我这样做:
Action foo = obj.Foo;
我每次都创建一个新的 Action
对象。 我敢肯定,这样做的代价是最小的,但是它涉及到分配内存以便以后进行垃圾回收。
鉴于委托人本身就是一成不变的,我想知道为什么他们不能成为值类型? 然后,上面的代码行只不过是对堆栈上的内存地址的简单分配而已*。
即使考虑使用匿名函数,(对我而言 )这似乎也可行。 考虑下面的简单示例。
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
代理)?
*是的,实现细节,我知道! 我真正的意思是短期内存存储 。
问题可以归结为:CLI(公共语言基础结构)规范说委托是引用类型。 为什么会这样呢?
原因在当今的.NET Framework中显而易见。 在原始设计中,有两种委托:普通委托和“多播”委托,它们的调用列表中可以有多个目标。 MulticastDelegate
类从Delegate
继承。 由于您不能从值类型继承,因此Delegate
必须是引用类型。
最后,所有实际的委托人最终都是多播委托,但是在此阶段的阶段,合并两个类为时已晚。 请参阅有关此确切主题的博客文章 :
在V1结束时,我们放弃了Delegate和MulticastDelegate之间的区别。 那时,合并这两个类将是一个巨大的变化,因此我们没有这样做。 您应该假装它们已合并并且仅存在MulticastDelegate。
此外,委托当前有4-6个字段,所有指针。 通常认为16字节为上限,在此上限下,节省内存仍然胜过额外的复制。 64位的MulticastDelegate
占用48个字节。 考虑到这一点,以及他们使用继承的事实表明,一个类是自然的选择。
委托需要成为类的原因只有一个,但这是一个很大的理由:尽管委托可以足够小以允许有效地存储为值类型(在32位系统上为8字节,在64位系统上为16字节)系统),它不可能足够小以有效地保证一个线程尝试编写委托,而另一个线程尝试执行委托,则后者线程最终不会在新目标上调用旧方法,也不会对旧目标的新方法。 允许发生这种事情将是一个主要的安全漏洞。 将委托作为引用类型可以避免这种风险。
实际上,将委托作为接口要比让委托成为结构类型更好。 创建一个闭包需要创建两个堆对象:一个由编译器生成的对象,用于保存任何封闭的变量;以及一个委托,用于对该对象调用适当的方法。 如果委托是接口,则包含封闭变量的对象本身可以用作委托,而无需其他对象。
想象一下委托是否为值类型。
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
的副本,这意味着将创建一个全新的CompilerGenerated
( handler
的副本)并将其传递给SignalEvent
。 SignalTwice
将执行两次委托,这将使副本中的counter
增加两次。 然后, SignalTwice
返回,并且该函数打印0,因为原始文件未被修改。
这是一个不为人知的猜测:
如果将委托实现为值类型,则实例的复制将非常昂贵,因为委托实例相对较重。 也许MS认为将它们设计为不可变的引用类型会更安全-将机器字大小的引用复制到实例相对便宜。
委托实例至少需要:
让我们假设值类型委托的实现方式与当前引用类型的实现方式类似(这可能有点不合理;很可能已经选择了其他设计来减小尺寸)。 使用Reflector,这是委托实例中必需的字段:
System.Delegate: _methodBase, _methodPtr, _methodPtrAux, _target
System.MulticastDelegate: _invocationCount, _invocationList
如果实现为结构(无对象标头),则这些将在x86上最多增加24个字节,在x64上最多增加48个字节,这对于一个结构来说是巨大的。
另一方面,我想问一下,在您建议的设计中,使CompilerGenerated
闭包类型成为结构有何帮助。 创建的委托的对象指针将指向何处? 在没有适当的转义分析的情况下将闭包类型实例留在堆栈中将是极具风险的业务。
我在互联网上看到了这个有趣的对话:
不可变并不意味着它必须是值类型。 值类型的东西不需要是不变的。 两者通常是并行的,但实际上它们不是同一回事,而.NET Framework中实际上每个实例都有反例(例如String类)。
答案是:
区别在于,虽然不变引用类型是相当普遍且完全合理的,但使值类型可变几乎总是一个坏主意,并且可能导致一些非常令人困惑的行为!
从这里取
因此,我认为该决定是由语言可用性方面而不是由编译器技术难题决定的。 我喜欢可为空的代表。
我可以说让委托作为引用类型绝对是一个错误的设计选择。 它们可以是值类型,并且仍支持多播委托。
假设Delegate是一个结构,例如:object target; 指向方法的指针
它可以是一个结构,对不对? 仅当目标是结构时才会进行装箱(但不会对代理本身进行装箱)。
您可能会认为它不支持MultiCastDelegate,但是我们可以:创建一个新对象,该对象将保存普通委托数组。 将Delegate(作为结构)返回到该新对象,该对象将实现Invoke对其所有值进行迭代并在其上调用Invoke。
因此,对于永远不会调用两个或多个处理程序的普通委托,它可以作为结构体。 不幸的是,.Net不会改变这一点。
另外,方差不需要将Delegate用作引用类型。 委托的参数应为引用类型。 毕竟,如果您传递的字符串是必需的对象(用于输入,而不是ref或out),则不需要强制转换,因为字符串已经是对象。
我想原因之一是对多类型转换委托的支持多类型转换委托比仅仅几个指示目标和方法的字段要复杂得多。
只能以这种形式进行的另一件事是委托方差。 这种差异要求在两种类型之间进行参考转换。
有趣的是,F#定义了它自己的函数指针类型,该类型与委托类似,但更为轻量。 但是我不确定这是值还是引用类型。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.