События в лямбда-выражениях - ошибка компилятора С#?

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

class Producer
{
    public event EventHandler MyEvent;
}

class Consumer
{
    public void MyHandler(object sender, EventArgs e) { /* ... */ }
}

class Listener
{
    public static void WireUp<TProducer, TConsumer>(
        Expression<Action<TProducer, TConsumer>> expr) { /* ... */ }
}

Событие будет подключено как:

Listener.WireUp<Producer, Consumer>((p, c) => p.MyEvent += c.MyHandler);

Однако это дает ошибку компилятора:

CS0832: Дерево выражений может не содержать оператор присваивания

Теперь это кажется разумным, особенно после чтения объяснения о том, почему деревья выражений не могут содержать присвоения. Однако, несмотря на синтаксис С#, += не является назначением, это вызов метода Producer::add_MyEvent, как мы видим из CIL, который создается, если мы просто подключаем событие нормально:

L_0001: newobj instance void LambdaEvents.Producer::.ctor()
L_0007: newobj instance void LambdaEvents.Consumer::.ctor()
L_000f: ldftn instance void LambdaEvents.Consumer::MyHandler(object, class [mscorlib]System.EventArgs)
L_0015: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
L_001a: callvirt instance void LambdaEvents.Producer::add_MyEvent(class [mscorlib]System.EventHandler)

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

Edit:

Обратите внимание, что вопрос: "Является ли это поведение ошибкой компилятора?". Извините, если я не понял, о чем я просил.

Изменить 2

После прочтения ответа Inferis, где он говорит, что "в этот момент + = считается назначением", это имеет смысл, потому что на данный момент компилятор, возможно, не знает, что он будет превращен в CIL.

Однако мне не разрешено писать явную форму вызова метода:

Listener.WireUp<Producer, Consumer>(
    (p, c) => p.add_MyEvent(new EventHandler(c.MyHandler)));

дает:

CS0571: 'Producer.MyEvent.add': не может явным образом вызывать оператор или аксессор

Итак, я думаю, вопрос сводится к тому, что += на самом деле означает в контексте событий С#. Означает ли это, что "вызывать метод добавления для этого события" или означает "добавить к этому событию все-таки undefined". Если это первое, то мне кажется, что это ошибка компилятора, тогда как если она последняя, ​​то она несколько неинтуитивная, но, возможно, не ошибка. Мысли?

Ответ 1

В разделе 7.16.3 спецификации, операторы + = и - = называются "Назначением события", что, безусловно, делает его похожим на оператор присваивания. Сам факт, что он в разделе 7.16 ( "Операторы присваивания" ) - довольно большой намек:) С этой точки зрения ошибка компилятора имеет смысл.

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

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

Ответ 2

+ = - это назначение, независимо от того, что он делает (например, добавить событие). С точки зрения парсера это все еще задание.

Вы пробовали

Listener.WireUp<Producer, Consumer>((p, c) => { p.MyEvent += c.MyHandler; } );

Ответ 3

Почему вы хотите использовать класс Expression? Измените Expression<Action<TProducer, TConsumer>> в вашем коде просто Action<TProducer, TConsumer>, и все должно работать так, как вам нужно. То, что вы здесь делаете, вынуждает компилятор рассматривать выражение лямбда как дерево выражений, а не делегат, и дерево выражений действительно не может содержать такие назначения (оно рассматривалось как назначение, потому что вы используете оператор + =, я верю). Теперь лямбда-выражение может быть преобразовано в любую форму (как указано в [MSDN] [1]). Просто используя делегат (что весь класс Action), такие "присваивания" совершенно верны. Я, возможно, неправильно понял проблему здесь (возможно, есть определенная причина, по которой вам нужно использовать дерево выражений?), Но похоже, что решение, к счастью, простое!

Изменить: Правильно, я понимаю вашу проблему немного лучше из комментария. Есть ли какая-то причина, по которой вы не можете просто передать p.MyEvent и c.MyHandler в качестве аргументов метода WireUp и присоединить обработчик событий в методе WireUp (для меня это также кажется лучше с дизайнерской точки зрения)... что не устраняет необходимость в дереве выражений? Я думаю, что лучше всего, если вы все равно избегаете деревьев выражений, поскольку они, как правило, довольно медленны по сравнению с делегатами.

Ответ 4

Собственно, что касается компилятора в этой точке, это назначение. Оператор + = перегружен, но компилятор не заботится об этом в точке. В конце концов, вы генерируете выражение через лямбда (которое в какой-то момент будет скомпилировано для реального кода) и никакого реального кода.

Итак, что делает компилятор, скажем: создайте выражение, в котором вы добавите c.MyHandler к текущему значению p.MyEvent и сохраните измененное значение обратно в p.MyEvent. И вы фактически выполняете задание, даже если в конце концов вы этого не сделаете.

Есть ли причина, по которой метод WireUp должен принимать выражение, а не только действие?

Ответ 5

Я думаю, проблема заключается в том, что помимо объекта Expression<TDelegate> дерево выражений не статически вводится с точки зрения компилятора. MethodCallExpression, а друзья не публикуют информацию о статическом типе.

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

Тем не менее я подумал о том, чтобы отправить это на microsoft.