为什么代表引用类型?

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个字节.鉴于此,以及他们使用继承这一事实表明,一个班级是自然的选择.

  • 我没有看到任何理由`委托`需要两个以上的字段(持有方法和目标).给定两个"单播"委托,可以通过让`Target`引用包含另外两个委托的数组来形成多播委托,并且`Method`指向静态方法将这样的数组作为其第一个参数并在其中调用委托.请注意,使用此类多播委托的`Method`和`Target`构建委托将产生正确形成的多播委托 - 不喜欢当前的情况.还要注意...... (3认同)
  • 我有点看到你要去哪里,但是因为`Delegate`是一个引用类型,所有委托必须是引用类型,对我来说并不完全正确吗?我的意思是,考虑`System.Enum`:它是一个引用类型,所有实际的枚举类型都从它继承; 但枚举是价值类型.这在CLI中是合法的,并且显然可以从编译器的末端开始.因此,仍有必要确定所有委托类型都是引用类型的进一步原因. (2认同)
  • `System.Enum` 不是*值类型!它是一个抽象类;亲自查看:http://msdn.microsoft.com/en-us/library/system.enum.aspx。 (2认同)

sup*_*cat 9

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

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

  • 这个答案给出了最真实的原因。不考虑这个问题时,委托可能真的是值类型。也有类似的类型,比如 `TypedReference`、`ArgIterator` 和各种句柄,它们也代表对某物的引用,它们都是值类型。 (2认同)

Ray*_*hen 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?
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

  • 由于委托可以有多个订阅者,并且结构必须是固定大小的,因此您可能必须对可以多播的订阅者数量进行硬限制(如果您选择的数量太大,那么您的委托就会得到非常大,或者您必须将订阅者保留在一个单独的对象中,如数组(在这种情况下,您无法避免创建引用类型). (6认同)
  • 委托是没有理由必须是允许多订阅工作的引用类型.如果delegate是一个结构,它将一个对象引用和一个指向一个接受这样一个对象的方法的指针结合起来,那么两个Action(整数)委托可以组合成一个Action of Action(整数)以及一个指向ExecuteAllActionsInArray方法的指针(如果有的话)委托将ExecuteAllActionsInArray作为其方法,附加数组中的委托可以复制到新数组中. (2认同)
  • @Raymond Chen:从线程安全来看,委托必须是类类型。如果线程安全不是问题,委托可以是一个不可变的结构,具有一个对象类型的字段和一个指向可以作​​用于该类型对象的方法的指针。构造两个独立的委托不需要创建任何堆对象。将它们组合成一个多播委托需要创建一个堆对象来保存原始对象,以及一个结构,该结构将保存对该堆对象的引用和一个指向将运行该对象中保存的委托的方法的指针。 (2认同)
  • @Raymond Chen:实践中创建的大多数代表都只有一个目标。即使有必要为具有多个目标的多播委托进行堆分配,避免为每个委托进行堆分配将是一个重大胜利。实际上,我怀疑将每个单目标委托简化为包含一个对象引用和一个函数指针的单个堆对象会比实际存在的更好。 (2认同)