为同一个匿名方法创建两个委托实例是不相等的

时间:2009-09-14 17:34:13

标签: c# .net delegates anonymous-methods equals

请考虑以下示例代码:

static void Main(string[] args)
{
   bool same = CreateDelegate(1) == CreateDelegate(1);
}

private static Action CreateDelegate(int x)
{
   return delegate { int z = x; };
}

您可以想象两个委托实例的比较是相同的,就像使用旧的命名方法(新的Action(MyMethod))时一样。它们并不相同,因为.NET Framework为每个委托实例提供了一个隐藏的闭包实例。由于这两个委托实例都将其Target属性设置为其各自的隐藏实例,因此它们不进行比较。一种可能的解决方案是为生成的IL提供匿名方法,以将当前实例(此指针)存储在委托的目标中。这将允许代表正确比较,并且从调试器的角度来看也有帮助,因为您将看到您的类是目标,而不是隐藏的类。

您可以在我提交给Microsoft的错误中阅读有关此问题的更多信息。错误报告还举例说明了我们使用此功能的原因,以及为什么我们认为应该更改它。如果您认为这也是一个问题,请通过提供评级和验证来帮助支持它。

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=489518

您能看到为什么不应该更改功能的可能原因吗?你认为这是解决问题的最佳方法,还是你建议我采取不同的方式?

5 个答案:

答案 0 :(得分:19)

我不是很倾向于认为这是一个“错误”。此外,您还假设CLR中存在一些根本不存在的行为。

这里要理解的重要一点是,每次调用CreateDelegate方法时,都会返回一个新的匿名方法(并初始化一个新的闭包类)。您似乎熟悉delegate关键字,以在内部使用某种类型的匿名方法池。 CLR肯定不会这样做。每次调用方法时都会在内存中创建匿名方法的委托(与lambda表达式一样),并且由于在这种情况下,相等运算符当然会比较 references ,这是预期的结果返回false

虽然您建议的行为在某些情况下可能会有一些好处,但实施起来可能会非常复杂,并且更有可能导致不可预测的情况。我认为在每次调用时生成新的匿名方法和委托的当前行为是正确的,我怀疑这也是您在Microsoft Connect上获得的反馈。

如果您非常坚持要求您在问题中描述的行为,则始终可以选择memoizing您的CreateDelegate函数,这样可以确保每次都返回相同的代理相同的参数。实际上,因为这很容易实现,这可能是微软不考虑在CLR中实现它的几个原因之一。

答案 1 :(得分:4)

我不知道这个问题的C#具体细节,但我研究了具有相同行为的VB.Net等效功能。

底线是这种行为是“按设计”,原因如下

首先,在这种情况下,封闭是不可避免的。您在匿名方法中使用了一段本地数据,因此需要关闭来捕获状态。每次调用此方法都必须创建一个新的闭包,原因有很多。因此,每个委托将指向该闭包上的实例方法。

在幕后,匿名方法/表达式由代码中的System.MulticastDelegate派生实例表示。如果您查看本课程的Equals方法,您会注意到2个重要的细节

  • 它已被密封,因此派生委托无法更改等于行为
  • Equals方法的一部分对对象进行参考比较

这使得连接到不同闭包的2个lambda表达式无法比较为equals。

答案 2 :(得分:3)

编辑:旧答案留在线下的历史价值......

CLR必须解决隐藏类被视为相等的情况,并考虑可以对捕获的变量进行的任何操作。

在这种特殊情况下,捕获的变量(x)在委托或捕获上下文中都不会更改 - 但我宁愿该语言不需要这种分析的复杂性。语言越复杂,理解起来就越困难。它必须区分这种情况和下面的情况,其中捕获变量的值在每次调用时都会改变 - 在那里,它会产生很大的差异,委托你调用;他们绝不平等。

我认为这种已经很复杂的情况(封闭经常被误解)并不会试图过于“聪明”并找出潜在的平等,这是完全合理的。

IMO,你应该肯定采取不同的路线。这些是Action的概念独立实例。通过强制代表目标伪造它是一个可怕的黑客IMO。


问题是您在生成的类中捕获x的值。这两个x变量是独立的,因此它们是不相等的代理。这是一个展示独立性的例子:

using System;

class Test
{
    static void Main(string[] args)
    {
        Action first = CreateDelegate(1);
        Action second = CreateDelegate(1);
        first();
        first();
        first();
        first();
        second();
        second();
    }

    private static Action CreateDelegate(int x)
    {
        return delegate 
        { 
            Console.WriteLine(x);
            x++;
        };
    }
}

输出:

1
2
3
4
1
2

编辑:换句话说,你的原始程序相当于:

using System;

class Test
{
    static void Main(string[] args)
    {
        bool same = CreateDelegate(1) == CreateDelegate(1);
    }

    private static Action CreateDelegate(int x)
    {
        return new NestedClass(x).ActionMethod;
    }

    private class Nested
    {
        private int x;

        internal Nested(int x)
        {
            this.x = x;
        }

        internal ActionMethod()
        {
            int z = x;
        }
    }
}

正如您所知,将创建两个单独的Nested实例,它们将成为两个代表的目标。他们是不平等的,所以代表们也是不平等的。

答案 3 :(得分:0)

我无法想到我曾经需要这样做的情况。如果我需要比较代理,我总是使用命名委托,否则这样的事情是可能的:

MyObject.MyEvent += delegate { return x + y; };

MyObject.MyEvent -= delegate { return x + y; };

这个例子对于演示这个问题并不是很好,但是我想可能会出现这样的情况:允许这样做可能会破坏设计时预期不允许这样做的现有代码。

我确信有一些内部实现细节也会让这个想法变得糟糕,但我不知道内部是如何实现匿名方法的。

答案 4 :(得分:0)

这种行为是有道理的,因为否则匿名方法会混淆(如果它们具有相同的名称,给定相同的主体)。

您可以将代码更改为:

static void Main(){   
    bool same = CreateDelegate(1) == CreateDelegate(1);
}

static Action<int> action = (x) => { int z = x; };

private static Action<int> CreateDelegate(int x){
    return action;
}

或者,最好,因为这是一种使用它的坏方法(加上你比较结果,而Action没有返回值...如果你想返回一个值,请使用Func&lt; ...&gt; ):

static void Main(){
    var action1 = action;
    var action2 = action;
    bool same = action1 == action2;  // TRUE, of course
}

static Action<int> action = (x) => { int z = x; };