Существует стандартный шаблон для событий в .NET - они используют тип delegate
, который принимает простой объект, называемый отправителем, а затем фактическую "полезную нагрузку" во втором параметре, который должен быть получен из EventArgs
.
Обоснование второго параметра, полученного из EventArgs
, кажется довольно ясным (см. . Стандартная библиотека .NET Framework Annotated Reference). Он предназначен для обеспечения бинарной совместимости между приемниками событий и источниками при разработке программного обеспечения. Для каждого события, даже если у него есть только один аргумент, мы выводим класс аргументов пользовательских событий, у которого есть одно свойство, содержащее этот аргумент, поэтому мы сохраняем возможность добавлять дополнительные свойства в полезную нагрузку в будущих версиях без нарушения существующего кода клиента, Очень важно в экосистеме самостоятельно разработанных компонентов.
Но я нахожу, что то же самое касается нулевых аргументов. Это означает, что если у меня есть событие, у которого нет аргументов в моей первой версии, и я пишу:
public event EventHandler Click;
... тогда я делаю это неправильно. Если я изменю тип делегата в будущем на новый класс в качестве его полезной нагрузки:
public class ClickEventArgs : EventArgs { ...
... Я нарушу двоичную совместимость с моими клиентами. Клиент заканчивается привязкой к определенной перегрузке внутреннего метода add_Click
, который принимает EventHandler
, и если я изменяю тип делегата, то они не могут найти эту перегрузку, поэтому существует MissingMethodException
.
Хорошо, так что, если я использую удобную общую версию?
public EventHandler<EventArgs> Click;
Нет, все еще неправильно, потому что EventHandler<ClickEventArgs>
не является EventHandler<EventArgs>
.
Итак, чтобы получить преимущество EventArgs
, вы должны извлечь из него, а не использовать его напрямую, как есть. Если вы этого не сделаете, вы можете не использовать его (мне кажется).
Тогда есть первый аргумент, sender
. Мне кажется, как рецепт нечестивой связи. Увольнение по событию - это вызов функции. Должна ли функция, вообще говоря, иметь возможность возвращаться через стек и выяснить, кто был вызывающим, и соответствующим образом скорректировать его поведение? Должны ли мы указывать, что интерфейсы должны выглядеть так?
public interface IFoo
{
void Bar(object caller, int actualArg1, ...);
}
В конце концов, разработчик Bar
может захотеть узнать, кем был caller
, поэтому они могут запрашивать дополнительную информацию! Надеюсь, вы сейчас пытаетесь. Почему это должно быть иначе для событий?
Итак, даже если я готов взять на себя боль сделать отдельный класс EventArgs
-derived для каждого объявляемого события, просто чтобы сделать его полезным при использовании EventArgs
вообще, я определенно предпочел бы отказаться от аргумент отправителя объекта.
Функция автозаполнения Visual Studio, похоже, не заботится о том, какой делегат вы используете для события - вы можете набрать +=
[hit Space, Return] и написать метод обработчика для вас, который соответствует любому делегату, каким он является.
Итак, какое значение я бы потерял, отклонившись от стандартного шаблона?
Как вопрос с бонусом, будет ли С#/CLR 4.0 что-либо менять, возможно, через контравариантность в делегатах? Я попытался исследовать это, но нажал еще одну проблему. Первоначально я включил этот аспект вопроса в этот другой вопрос, но это вызвало путаницу. И, похоже, немного расщепить это на три вопроса...
Update:
Оказывается, я был прав, чтобы узнать о влиянии контравариантности на весь этот вопрос!
Как отметил в другом месте, новые правила компилятора оставляют дыру в системе типов, которая взрывается во время выполнения. Отверстие эффективно подключено путем определения EventHandler<T>
по-разному Action<T>
.
Итак, для событий, чтобы избежать этого отверстия, вы не должны использовать Action<T>
. Это не значит, что вы должны использовать EventHandler<TEventArgs>
; это просто означает, что если вы используете общий тип делегата, не выбирайте тот, который включен для контравариантности.