Подтвердить что ты не робот

Создание двух экземпляров делегата для одного и того же анонимного метода не равно

Рассмотрим следующий пример кода:

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

Вы можете увидеть любую возможную причину, по которой функциональность не должна изменяться? Считаете ли вы, что это лучший способ решения проблемы, или вы рекомендуете мне пойти по другому пути?

4b9b3361

Ответ 1

Я не настолько склонен думать, что это "ошибка". Более того, более того, вы предполагаете какое-то поведение в CLR, которого просто не существует.

Важно понять, что вы возвращаете новый анонимный метод (и инициализируете новый класс закрытия) каждый раз, когда вы вызываете метод CreateDelegate. Кажется, что вы используете ключевое слово delegate, чтобы использовать какой-то пул для анонимных методов внутри. CLR, конечно, этого не делает. Делегат анонимного метода (как с выражением лямбда) создается в памяти каждый раз, когда вы вызываете метод, и, поскольку оператор равенства, конечно, сравнивает ссылки в этой ситуации, это ожидаемый результат возвращает false.

Хотя ваше предполагаемое поведение может иметь определенные преимущества в определенных контекстах, это, вероятно, будет довольно сложно реализовать и, скорее всего, приведет к непредсказуемым сценариям. Я думаю, что текущее поведение генерации нового анонимного метода и делегирования на каждом вызове является правильным, и я подозреваю, что это обратная связь, которую вы также получите в Microsoft Connect.

Если вы действительно настаиваете на том, чтобы поведение, описанное в вашем вопросе, всегда имело возможность memoizing вашей функции CreateDelegate который гарантирует, что один и тот же делегат будет возвращаться каждый раз для тех же параметров. Действительно, поскольку это так просто реализовать, вероятно, это одна из нескольких причин, по которым Microsoft не рассматривала возможность ее реализации в среде CLR.

Ответ 2

Я не знаю о специфических деталях этой проблемы, но я работал над эквивалентной функцией VB.Net, которая имеет такое же поведение.

Суть в том, что это поведение "По дизайну" по следующим причинам.

Во-первых, в этом случае замыкание неизбежно. Вы использовали фрагмент локальных данных в анонимном методе, и, следовательно, закрытие необходимо для захвата состояния. Каждый вызов этого метода должен создать новое замыкание по ряду причин. Поэтому каждый делегат указывает на метод экземпляра при этом закрытии.

Под капотом анонимный метод/выражение представлен производным экземпляром System.MulticastDelegate в коде. Если вы посмотрите на метод Equals этого класса, вы заметите две важные детали.

  • Он запечатан, поэтому нет никакого способа, чтобы производный делегат изменял поведение равных.
  • Часть метода Equals выполняет сравнительное сравнение объектов

Это делает невозможным использование 2 лямбда-выражений, которые привязаны к разным замыканиям для сравнения как равных.

Ответ 3

EDIT: Старый ответ оставлен для исторического значения ниже строки...

CLR должен будет определить случаи, когда скрытые классы можно считать равными, принимая во внимание все, что можно было бы сделать с захваченными переменными.

В этом конкретном случае захваченная переменная (x) не изменяется ни внутри делегата, ни в контексте захвата, но я бы предпочел, чтобы язык не требовал такой сложности анализа. Чем сложнее язык, тем труднее его понимать. Это должно было бы различать этот случай и тот, который приведен ниже, где значение захваченного значения изменяется при каждом вызове - там, он делает большую разницу, которая делегирует вам вызов; они никоим образом не равны.

Я думаю, что совершенно разумно, что эта уже сложная ситуация (закрытие часто неправильно понимается) не пытается быть слишком "умной" и выработать потенциальное равенство.

ИМО, вы обязательно должны пойти по другому пути. Это концептуально независимые экземпляры Action. Подделка этого путем принуждения целей делегата - это ужасный взлом ИМО.


Проблема заключается в том, что вы захватываете значение 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

EDIT: чтобы посмотреть на это по-другому, ваша оригинальная программа была эквивалентна:

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, и они станут целями для двух делегатов. Они неравны, поэтому делегаты также неравны.

Ответ 4

Я не могу думать о ситуации, когда мне когда-либо нужно было это делать. Если мне нужно сравнивать делегаты, я всегда использую имена делегатов, иначе что-то подобное получится:

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

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

Этот пример не очень хорош для демонстрации проблемы, но я бы предположил, что может быть ситуация, при которой это может нарушить существующий код, который был разработан с ожиданием того, что это не разрешено.

Я уверен, что есть внутренние детали реализации, которые также делают это плохой идеей, но я точно не знаю, как анонимные методы реализованы внутри.

Ответ 5

Такое поведение имеет смысл, потому что в противном случае анонимные методы будут запутаны (если бы они имели одно и то же имя, учитывая одно и то же тело).

Вы можете изменить свой код на это:

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