Dan*_*Tao 35 .net c# delegates value-type reference-type
关于接受的答案的快速说明:我不同意Jeffrey的一小部分答案,即Delegate必须是一个引用类型,因此所有代表都是引用类型.(多层继承链排除值类型并不是真的;例如,所有枚举类型都System.Enum继承自System.ValueType,而继承自所有引用类型的继承System.Object,但是我认为事实是从根本上说,所有代表事实上都不仅仅是继承而是来自这里的关键实现.正如雷蒙指出,在一个评论他的答案,一旦你承诺支持多用户,有真的没有一点不使用引用类型的代表本身,因为需要一个数组的地方.DelegateMulticastDelegate
请参阅底部的更新.
如果我这样做,我一直觉得很奇怪:
Action foo = obj.Foo;
Run Code Online (Sandbox Code Playgroud)
我每次都在创建一个新 Action对象.我确信成本很低,但它涉及到内存分配以便以后进行垃圾回收.
鉴于委托本身就是不可变的,我想知道为什么它们不能成为价值类型?然后像上面那样的一行代码只会对堆栈上的内存地址进行简单的赋值*.
即使考虑匿名功能,似乎(对我而言)这也行得通.请考虑以下简单示例.
Action foo = () => { obj.Foo(); };
Run Code Online (Sandbox Code Playgroud)
在这种情况下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;
Run Code Online (Sandbox Code Playgroud)
这个问题有意义吗?在我看来,有两种可能的解释:
最后,我不会因此而失眠; 这只是一段时间以来我一直很好奇的事情.
更新:为了回应Ani的评论,我明白为什么CompilerGenerated上面例子中的类型也可能是一个引用类型,因为如果一个委托将包含一个函数指针和一个对象指针,它无论如何都需要一个引用类型(at至少对于使用闭包的匿名函数,因为即使你引入了一个额外的泛型类型参数 - 例如,Action<TCaller>- 这不会涵盖无法命名的类型!).但是,所有这一切都让我感到遗憾的是将编译器生成的闭包类型的问题带入讨论中!我的主要问题是关于委托,即带有函数指针和对象指针的东西.在我看来,它可能是一种价值类型.
换句话说,即使这......
Action foo = () => { obj.Foo(); };
Run Code Online (Sandbox Code Playgroud)
......需要创建一个引用类型对象(支持关闭,并给委托一些参考),为什么它需要创建2(封支撑对象加了Action代表)?
*是的,是的,实施细节,我知道!我真正的意思是短期记忆存储.
Jef*_*Sax 16
问题归结为:CLI(公共语言基础结构)规范说委托是引用类型.为什么会这样?
今天在.NET Framework中可以清楚地看到一个原因.在原始设计中,有两种委托:普通委托和"多播"委托,它们的调用列表中可以有多个目标.将MulticastDelegate类从继承Delegate.由于您无法从值类型继承,Delegate因此必须是引用类型.
最后,所有实际的代表最终都成为了多播委托,但在这个过程的那个阶段,合并这两个类为时已晚.有关此确切主题,请参阅此博客文章:
我们在V1结束时放弃了Delegate和MulticastDelegate之间的区别.那时, 合并两个班级将是一个巨大的变化,所以我们没有这样做.您应该假装它们已合并,并且只存在MulticastDelegate.
此外,代表们目前有4-6个字段,所有指针.16字节通常被认为是上限,其中保存内存仍然胜过额外复制.64位MulticastDelegate占用48个字节.鉴于此,以及他们使用继承这一事实表明,一个班级是自然的选择.
Delegate需要成为一个类只有一个原因,但它是一个很大的原因:虽然委托可能小到足以允许有效存储作为值类型(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?
Run Code Online (Sandbox Code Playgroud)
根据您的提议,这将在内部转换为
struct CompilerGenerated
{
int counter = 0;
public Execute() { ++counter; }
};
Notify handler = new CompilerGenerated();
SignalTwice(handler);
System.Console.WriteLine(counter); // what should this print?
Run Code Online (Sandbox Code Playgroud)
如果delegate是值类型,那么SignalEvent将得到一个副本handler,这意味着CompilerGenerated将创建一个全新的(副本handler)并传递给SignalEvent.SignalTwice将执行两次委托,在副本中增加counter两次.然后返回,并且函数打印0,因为原始文件未被修改.SignalTwice
| 归档时间: |
|
| 查看次数: |
10387 次 |
| 最近记录: |