Почему лямбда-выражения не "интернированы"?
Строки являются ссылочными типами, но они неизменяемы. Это позволяет им быть интернированным компилятором; везде появляется один и тот же строковый литерал, на который можно ссылаться.
Делегаты также являются неизменяемыми ссылочными типами. (Добавление метода к делегату многоадресной рассылки с использованием оператора +=
представляет собой присвоение, это не изменчивость.) И, например, строки, существует "буквальный" способ представления делегата в коде с использованием лямбда-выражения, например:
Func<int> func = () => 5;
Правая часть этого оператора - выражение, тип которого Func<int>
; но я никоим образом не ссылаюсь на конструктор Func<int>
(а также неявное преобразование). Поэтому я рассматриваю это как по существу буквальное. Я ошибаюсь в своем определении "буквальный" здесь?
Независимо, вот мой вопрос. Если у меня есть две переменные для, скажем, типа Func<int>
, и я назначаю одинаковые лямбда-выражения для обоих:
Func<int> x = () => 5;
Func<int> y = () => 5;
... что мешает компилятору рассматривать их как один и тот же объект Func<int>
?
Я спрашиваю, потому что в разделе 6.5.1 спецификации С# 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();
}
}
Есть ли причина, почему это поведение - один экземпляр делегата для семантически идентичных анонимных методов - не реализован?
Ответы
Ответ 1
Вы ошибаетесь, чтобы назвать это буквальным, ИМО. Это просто выражение, которое конвертируется в тип делегата.
Теперь, что касается части "интернирования" - некоторые лямбда-выражения кэшируются, в том, что для одного выражения лямбда иногда может быть создан и повторно использован один экземпляр, однако часто встречается такая строка кода. Некоторые из них не обрабатываются таким образом: обычно это зависит от того, отражает ли лямбда-выражение любые нестатические переменные (будь то через "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
, поэтому "второй" в конце не равен нулю.
Interning lambdas, которые появляются в разных местах кода, но с тем же исходным кодом, - это другое дело. Я подозреваю, что было бы довольно сложно сделать это правильно (в конце концов, тот же исходный код может означать разные вещи в разных местах), и я, конечно, не хотел бы полагаться на это. Если это не стоит полагаться на это, и это большая работа, чтобы получить право на команду компилятора, я не думаю, что это лучший способ провести свое время.
Ответ 2
Есть ли причина, почему это поведение - один экземпляр делегата для семантически идентичных анонимных методов - не реализован?
Да. Поскольку тратить время на сложную оптимизацию, которая приносит пользу почти никому, уходит много времени от разработки, внедрения, тестирования и поддержки функций, которые приносят пользу людям.
Ответ 3
Lambdas не может быть интернирован, потому что они используют объект для хранения захваченных локальных переменных. И этот экземпляр отличается каждый раз, когда вы строите делегат.
Ответ 4
В общем, нет никакого различия между строковыми переменными, которые относятся к одному экземпляру String, по сравнению с двумя переменными, которые относятся к различным строкам, которые содержат одну и ту же последовательность символов. То же самое относится и к делегатам.
Предположим, у меня есть два делегата, назначенных двум различным лямбда-выражениям. Затем я подписываю обоих делегатов на обработчик событий и отписываю их. Каким должен быть результат?
Было бы полезно, если бы в vb или С# был способ указать, что анонимный метод или лямбда, который не ссылается на Me/this, следует рассматривать как статический метод, создавая один делегат, который можно использовать повторно срок службы приложения. Нет синтаксиса, чтобы указать, что, однако, и для компилятора, чтобы решить, что разные лямбда-выражения возвращают один и тот же экземпляр, будет потенциально нарушающим изменение.
EDIT Я предполагаю, что спецификация позволяет это, даже если это может быть потенциально нарушающим изменение, если какой-либо код должен полагаться на разные экземпляры.
Ответ 5
Другие ответы приносят хорошие моменты. Mine действительно не имеет ничего общего с техническим - Каждая функция начинается с -100 пунктов.
Ответ 6
Это разрешено, потому что команда С# не может контролировать это. Они в значительной степени полагаются на детали реализации делегатов (CLR + BCL) и оптимизатор компилятора JIT. В настоящее время существует довольно широкое распространение CLR и реализаций дрожания, и нет оснований предполагать, что это закончится. Спецификация CLI очень легка в правилах относительно делегатов, которые недостаточно сильны, чтобы гарантировать, что все эти разные команды получат реализацию, которая гарантирует согласованность равенства делегата. Не в последнюю очередь потому, что это мешало будущим инновациям. Здесь можно оптимизировать.