12

考虑以下示例代码:

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

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

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

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

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

您能看出不应更改功能的任何可能原因吗?您认为这是解决问题的最佳做法,还是建议我采取不同的方法?

4

5 回答 5

19

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

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

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

如果您非常坚持要具有您在问题中描述的行为,则始终可以选择记住您的CreateDelegate函数,这将确保每次为相同的参数返回相同的委托。事实上,因为这很容易实现,这可能是微软没有考虑在 CLR 中实现它的几个原因之一。

于 2009-09-14T17:40:06.747 回答
5

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

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将创建两个单独的实例,它们将成为两个委托的目标。他们是不平等的,所以代表也是不平等的。

于 2009-09-14T17:51:45.020 回答
4

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

归根结底,这种行为是“设计使然”,原因如下

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

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

  • 它是密封的,因此派生委托无法更改 equals 行为
  • Equals 方法的一部分对对象进行引用比较

这使得附加到不同闭包的 2 个 lambda 表达式无法进行相等比较。

于 2009-09-14T17:41:36.737 回答
1

我想不出我曾经需要这样做的情况。如果我需要比较委托,我总是使用命名委托,否则可能会出现这样的情况:

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

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

这个例子不是很好地展示这个问题,但我想可能会有这样的情况,允许这样做可能会破坏现有的代码,而这些代码的设计期望是不允许这样做的。

我确信有内部实现细节也使这成为一个坏主意,但我不确切知道匿名方法是如何在内部实现的。

于 2009-09-14T17:43:10.920 回答
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<...> ):

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

static Action<int> action = (x) => { int z = x; };
于 2009-09-14T20:04:41.307 回答