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

时间:2021-02-09 16:18:43

Consider the following example code:

请考虑以下示例代码:

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

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

You would imagine that the two delegate instances would compare to be equal, just as they would when using the good old named method approach (new Action(MyMethod)). They do not compare to be equal because the .NET Framework provides a hidden closure instance per delegate instance. Since those two delegate instances each have their Target properties set to their individual hidden instance, they do not compare. One possible solution is for the generated IL for an anonymous method to store the current instance (this pointer) in the target of the delegate. This will allow the delegates to compare correctly, and also helps from a debugger standpoint since you will see your class being the target, instead of a hidden class.

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

You can read more about this issue in the bug I submitted to Microsoft. The bug report also gives an example of why we are using this functionality, and why we feel it should be changed. If you feel this to be an issue as well, please help support it by providing rating and validation.

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

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

Can you see any possible reason why the functionality should not be changed? Do you feel this was the best course of action to get the issue resolved, or do you recommend that I should take a different route?

您能看到为什么不应该更改功能的任何可能原因吗?您是否认为这是解决问题的最佳方法,或者您是否建议我采取不同的路线?

5 个解决方案

#1


19  

I'm not so inclined to think this is a "bug". It appears moreover that you're assuming some behaviour in the CLR that simply does not exist.

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

The important thing to understand here is that you are returning a new anonymous method (and initialising a new closure class) each time you call the CreateDelegate method. It seems that you are experting the delegate keyword to use some sort of pool for anonymous methods internally. The CLR certainly does not do this. A delegate to the anonymous method (as with a lambda expression) is created in memory each time you call the method, and since the equality operator does of course compare references in this situation, it is the expected result to return false.

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

Although your suggested behaviour may have some benefits in certain contexts, it would probably be quite complicated to implement, and would more likely lead to unpredictable scenarios. I think the current behaviour of generating a new anonymous method and delegate on each call is the right one, and I suspect this is the feedback you will get on Microsoft Connect as well.

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

If you are quite insistent on having the behaviour you described in your question, there is always the option of memoizing your CreateDelegate function, which would insure that the same delegate is returned each time for the same parameters. Indeed, because this is so easy to implement, it is probably one of the several reasons why Microsoft did not consider implementing it in the CLR.

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

#2


4  

I don't know about the C# specific details of this problem but I worked on the VB.Net equivalent feature which has the same behavior.

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

The bottom line is this behavior is "By Design" for the following reasons

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

The first is that in this scenario a closure is unavoidable. You have used a piece of local data within an anonymous method and hence a closure is necessary to capture the state. Every call to this method must create a new closure for a number of reasons. Therefore each delegate will point to an instance method on that closure.

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

Under the hood a anonymous method / expression is represented by a System.MulticastDelegate derived instance in code. If you look at the Equals method of this class you will notice 2 important details

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

  • It is sealed so there is no way for a derived delegate to change the equals behavior
  • 它是密封的,因此派生委托无法改变等于行为

  • Part of the Equals method does a reference comparison on the objects
  • Equals方法的一部分对对象进行参考比较

This makes it impossible for 2 lambda expressions which are attached to different closures to compare as equals.

这使得连接到不同闭包的2个lambda表达式不可能作为等于进行比较。

#3


3  

EDIT: Old answer left for historical value below the line...

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

The CLR would have to work out the cases in which the hidden classes could be considered equal, taking into account anything that could be done with the captured variables.

CLR必须考虑到隐藏类可以被认为是相等的情况,并考虑到可以用捕获的变量完成的任何事情。

In this particular case, the captured variable (x) isn't changed either within the delegate or in the capturing context - but I'd rather the language didn't require this sort of complexity of analysis. The more complicated the language is, the harder it is to understand. It would have to distinguish between this case and the one below, where the captured variable's value is changed on each invocation - there, it makes a great deal of difference which delegate you call; they are in no way equal.

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

I think it's entirely sensible that this already-complex situation (closures are frequently misunderstood) doesn't try to be too "clever" and work out potential equality.

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

IMO, you should definitely take a different route. These are conceptually independent instances of Action. Faking it by coercing the delegate targets is a horrible hack IMO.

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


The problem is that you're capturing the value of x in a generated class. The two x variables are independent, so they're unequal delegates. Here's an example demonstrating the independence:

问题是你在生成的类中捕获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++;
        };
    }
}

Output:

1
2
3
4
1
2

EDIT: To look at it another way, your original program was the equivalent of:

编辑:以另一种方式来看,你的原始程序相当于:

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;
        }
    }
}

As you can tell, two separate instances of Nested will be created, and they will be the targets for the two delegates. They are unequal, so the delegates are unequal too.

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

#4


0  

I can't think of a situation where I've ever needed to do that. If I need to compare delegates I always use named delegates, otherwise something like this would be possible:

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

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

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

This example isn't great for demonstrating the issue, but I would imagine that there could be a situation where allowing this could break existing code that was designed with the expectation that this is not allowed.

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

I'm sure there are internal implementation details that also make this a bad idea, but I don't know exactly how anonymous methods are implemented internally.

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

#5


0  

This behaviour makes sense because otherwise anonymous methods would get mixed up (if they had the same name, given the same body).

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

You could change your code to this:

您可以将代码更改为:

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;
}

Or, preferably, since that's a bad way to use it (plus you were comparing the result, and Action doesn't have a return value ... use Func<...> if you want to return a value):

或者,最好,因为这是一种使用它的坏方法(加上你比较结果,而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; };

#1


19  

I'm not so inclined to think this is a "bug". It appears moreover that you're assuming some behaviour in the CLR that simply does not exist.

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

The important thing to understand here is that you are returning a new anonymous method (and initialising a new closure class) each time you call the CreateDelegate method. It seems that you are experting the delegate keyword to use some sort of pool for anonymous methods internally. The CLR certainly does not do this. A delegate to the anonymous method (as with a lambda expression) is created in memory each time you call the method, and since the equality operator does of course compare references in this situation, it is the expected result to return false.

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

Although your suggested behaviour may have some benefits in certain contexts, it would probably be quite complicated to implement, and would more likely lead to unpredictable scenarios. I think the current behaviour of generating a new anonymous method and delegate on each call is the right one, and I suspect this is the feedback you will get on Microsoft Connect as well.

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

If you are quite insistent on having the behaviour you described in your question, there is always the option of memoizing your CreateDelegate function, which would insure that the same delegate is returned each time for the same parameters. Indeed, because this is so easy to implement, it is probably one of the several reasons why Microsoft did not consider implementing it in the CLR.

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

#2


4  

I don't know about the C# specific details of this problem but I worked on the VB.Net equivalent feature which has the same behavior.

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

The bottom line is this behavior is "By Design" for the following reasons

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

The first is that in this scenario a closure is unavoidable. You have used a piece of local data within an anonymous method and hence a closure is necessary to capture the state. Every call to this method must create a new closure for a number of reasons. Therefore each delegate will point to an instance method on that closure.

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

Under the hood a anonymous method / expression is represented by a System.MulticastDelegate derived instance in code. If you look at the Equals method of this class you will notice 2 important details

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

  • It is sealed so there is no way for a derived delegate to change the equals behavior
  • 它是密封的,因此派生委托无法改变等于行为

  • Part of the Equals method does a reference comparison on the objects
  • Equals方法的一部分对对象进行参考比较

This makes it impossible for 2 lambda expressions which are attached to different closures to compare as equals.

这使得连接到不同闭包的2个lambda表达式不可能作为等于进行比较。

#3


3  

EDIT: Old answer left for historical value below the line...

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

The CLR would have to work out the cases in which the hidden classes could be considered equal, taking into account anything that could be done with the captured variables.

CLR必须考虑到隐藏类可以被认为是相等的情况,并考虑到可以用捕获的变量完成的任何事情。

In this particular case, the captured variable (x) isn't changed either within the delegate or in the capturing context - but I'd rather the language didn't require this sort of complexity of analysis. The more complicated the language is, the harder it is to understand. It would have to distinguish between this case and the one below, where the captured variable's value is changed on each invocation - there, it makes a great deal of difference which delegate you call; they are in no way equal.

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

I think it's entirely sensible that this already-complex situation (closures are frequently misunderstood) doesn't try to be too "clever" and work out potential equality.

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

IMO, you should definitely take a different route. These are conceptually independent instances of Action. Faking it by coercing the delegate targets is a horrible hack IMO.

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


The problem is that you're capturing the value of x in a generated class. The two x variables are independent, so they're unequal delegates. Here's an example demonstrating the independence:

问题是你在生成的类中捕获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++;
        };
    }
}

Output:

1
2
3
4
1
2

EDIT: To look at it another way, your original program was the equivalent of:

编辑:以另一种方式来看,你的原始程序相当于:

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;
        }
    }
}

As you can tell, two separate instances of Nested will be created, and they will be the targets for the two delegates. They are unequal, so the delegates are unequal too.

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

#4


0  

I can't think of a situation where I've ever needed to do that. If I need to compare delegates I always use named delegates, otherwise something like this would be possible:

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

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

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

This example isn't great for demonstrating the issue, but I would imagine that there could be a situation where allowing this could break existing code that was designed with the expectation that this is not allowed.

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

I'm sure there are internal implementation details that also make this a bad idea, but I don't know exactly how anonymous methods are implemented internally.

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

#5


0  

This behaviour makes sense because otherwise anonymous methods would get mixed up (if they had the same name, given the same body).

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

You could change your code to this:

您可以将代码更改为:

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;
}

Or, preferably, since that's a bad way to use it (plus you were comparing the result, and Action doesn't have a return value ... use Func<...> if you want to return a value):

或者,最好,因为这是一种使用它的坏方法(加上你比较结果,而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; };