Почему лямбда-выражения не «интернированы»? - PullRequest
15 голосов
/ 26 января 2011

Строки являются ссылочными типами, но они неизменны. Это позволяет им быть интернированными компилятором; везде, где появляется один и тот же строковый литерал, на один и тот же объект можно ссылаться.

Делегаты также являются неизменяемыми ссылочными типами. (Добавление метода к многоадресному делегату с использованием оператора += составляет присваивание ; это не изменчивость.) И, как и строки, существует «буквальный» способ представления делегата в коде с использованием лямбда-выражение, например:

Func<int> func = () => 5;

Правая часть этого оператора является выражением, тип которого Func<int>; но нигде я не вызываю явно конструктор Func<int> (не происходит и неявное преобразование). Поэтому я рассматриваю это как буквально литерал . Я ошибаюсь в своем определении «буквальный» здесь?

В любом случае, вот мой вопрос. Если у меня есть две переменные для, скажем, типа Func<int>, и я назначаю одинаковые лямбда-выражения для обоих:

Func<int> x = () => 5;
Func<int> y = () => 5;

... что мешает компилятору рассматривать их как один и тот же объект Func<int>?

Я спрашиваю, потому что в разделе 6.5.1 спецификации языка C # 4.0 четко указано:

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

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

class EventSender
{
    public event EventHandler Event;
    public void Send()
    {
        EventHandler handler = this.Event;
        if (handler != null) { handler(this, EventArgs.Empty); }
    }
}

class Program
{
    static string _message = "Hello, world!";

    static void Main()
    {
        var sender = new EventSender();
        sender.Event += (obj, args) => Console.WriteLine(_message);
        sender.Send();

        // Unless I'm mistaken, this lambda expression is semantically identical
        // to the one above. However, the handler is not removed, indicating
        // that a different delegate instance is constructed.
        sender.Event -= (obj, args) => Console.WriteLine(_message);

        // This prints "Hello, world!" again.
        sender.Send();
    }
}

Есть ли какая-либо причина, по которой это поведение - один экземпляр делегата для семантически идентичных анонимных методов - не реализовано?

Ответы [ 6 ]

11 голосов
/ 26 января 2011

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

Теперь, что касается "интернирующей" части - некоторые лямбда-выражения кэшируются , то есть для одного лямбда-выражения, иногда одногоЭкземпляр может быть создан и использован повторно, однако часто встречается эта строка кода.Некоторые не обрабатываются таким образом: обычно это зависит от того, захватывает ли лямбда-выражение какие-либо нестатические переменные (будь то через "this" или локально для метода).

Вот пример такого кэширования:

using System;

class Program
{
    static void Main()
    {
        Action first = GetFirstAction();
        first -= GetFirstAction();
        Console.WriteLine(first == null); // Prints True

        Action second = GetSecondAction();
        second -= GetSecondAction();
        Console.WriteLine(second == null); // Prints False
    }

    static Action GetFirstAction()
    {
        return () => Console.WriteLine("First");
    }

    static Action GetSecondAction()
    {
        int i = 0;
        return () => Console.WriteLine("Second " + i);
    }
}

В этом случае мы можем видеть, что первое действие было кэшировано (или, по крайней мере, два равных делегата были созданы, и фактически Reflector показывает, что на самом деле равен кэшируется в статическом поле).Второе действие создало два неравных экземпляра Action для двух вызовов GetSecondAction, поэтому «second» не является ненулевым в конце.

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

6 голосов
/ 27 января 2011

Есть ли причина, по которой это поведение - один экземпляр делегата для семантически идентичных анонимных методов - не реализовано?

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

1 голос
/ 26 января 2011

Это разрешено, потому что команда C # не может это контролировать. Они в значительной степени зависят от деталей реализации делегатов (CLR + BCL) и оптимизатора JIT-компилятора. В настоящее время уже существует значительное количество реализаций CLR и джиттера, и нет оснований предполагать, что это закончится. Спецификация CLI очень легка в отношении правил для делегатов, но недостаточно сильна, чтобы гарантировать, что все эти разные группы получат реализацию, гарантирующую согласованность равенства объектов делегатов. Не в последнюю очередь потому, что это будет препятствовать будущим инновациям. Здесь есть что оптимизировать.

1 голос
/ 26 января 2011

Другие ответы поднимают хорошие моменты.Мой действительно не имеет ничего общего с техническими - Каждая функция начинается с -100 баллов .

1 голос
/ 26 января 2011

Как правило, нет различия между строковыми переменными, которые ссылаются на один и тот же экземпляр String, и двумя переменными, которые ссылаются на разные строки, которые содержат одинаковую последовательность символов.То же самое не относится к делегатам.

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

Было бы полезно, если бы в vb или C # был способ указать, что анонимный метод или лямбда, который не ссылается на Me / это следует рассматривать как статический метод,создание одного делегата, который можно использовать повторно в течение всего срока действия приложения.Однако нет синтаксиса, указывающего на это, и для компилятора, который решит, что разные лямбда-выражения возвращают один и тот же экземпляр, было бы потенциально критическое изменение.это, хотя это может быть потенциально разрушительное изменение, если какой-либо код будет полагаться на отличающиеся экземпляры.

1 голос
/ 26 января 2011

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

...