Подпись события в.NET - Использование строго отправленного типа "Отправитель"?
Я полностью понимаю, что то, что я предлагаю, не соответствует рекомендациям.NET, и, следовательно, вероятно, является плохой идеей только по этой причине. Тем не менее, я хотел бы рассмотреть это с двух возможных точек зрения:
(1) Должен ли я рассмотреть возможность использования этого для моей собственной разработки, которая на 100% предназначена для внутренних целей.
(2) Является ли это концепцией, которую разработчики фреймворка могли бы рассмотреть для изменения или обновления?
Я думаю об использовании сигнатуры события, которая использует строго типизированный "отправитель", вместо того, чтобы вводить его как "объект", который является текущим шаблоном проектирования.NET. То есть вместо использования стандартной подписи события, которая выглядит следующим образом:
class Publisher
{
public event EventHandler<PublisherEventArgs> SomeEvent;
}
Я рассматриваю возможность использования подписи события, которая использует строгое типизированный параметр "отправитель", а именно:
Сначала определите "StrongTypedEventHandler":
[SerializableAttribute]
public delegate void StrongTypedEventHandler<TSender, TEventArgs>(
TSender sender,
TEventArgs e
)
where TEventArgs : EventArgs;
Это не все, что отличается от действия StrongTypedEventHandler
мы обеспечиваем, чтобы TEventArgs происходил из System.EventArgs
,
Далее, в качестве примера, мы можем использовать StrongTypedEventHandler в классе публикации следующим образом:
class Publisher
{
public event StrongTypedEventHandler<Publisher, PublisherEventArgs> SomeEvent;
protected void OnSomeEvent()
{
if (SomeEvent != null)
{
SomeEvent(this, new PublisherEventArgs(...));
}
}
}
Приведенная выше схема позволит подписчикам использовать обработчик событий со строгой типизацией, который не требует приведения:
class Subscriber
{
void SomeEventHandler(Publisher sender, PublisherEventArgs e)
{
if (sender.Name == "John Smith")
{
// ...
}
}
}
Я полностью осознаю, что это нарушает стандартный шаблон обработки событий.NET; однако, имейте в виду, что контравариантность позволит подписчику использовать традиционную подпись обработки событий, если это необходимо:
class Subscriber
{
void SomeEventHandler(object sender, PublisherEventArgs e)
{
if (((Publisher)sender).Name == "John Smith")
{
// ...
}
}
}
То есть, если обработчику событий необходимо подписаться на события из разнородных (или, возможно, неизвестных) типов объектов, обработчик может ввести параметр "отправитель" как "объект", чтобы обработать всю ширину потенциальных объектов отправителя.
Кроме нарушения соглашения (это то, что я не воспринимаю легкомысленно, поверьте мне), я не могу думать о каких-либо недостатках этого.
Здесь могут быть некоторые проблемы соответствия CLS. Это действительно работает в Visual Basic .NET 2008 на 100% (я тестировал), но я считаю, что более старые версии Visual Basic.NET до 2005 года не имеют ковариации и контравариантности делегатов. [Редактировать: с тех пор я проверил это, и это подтверждается: VB.NET 2005 и ниже не могут справиться с этим, но VB.NET 2008 на 100% хорошо. См. "Редактирование #2" ниже.] Могут быть и другие языки.NET, в которых также есть проблема, я не уверен.
Но я не вижу себя разработчиком для какого-либо языка, кроме C# или Visual Basic .NET, и я не против ограничить его C# и VB.NET для.NET Framework 3.0 и выше. (Если честно, я не мог представить, что вернусь к 2.0.)
Может ли кто-нибудь еще думать о проблеме с этим? Или это просто настолько нарушает конвенцию, что заставляет людей вращаться?
Вот некоторые ссылки, которые я нашел:
(1) Руководство по разработке мероприятий [MSDN 3.5]
(3) Шаблон подписи события в.net [Stackru 2008]
Меня интересует мнение всех и каждого по этому поводу...
Заранее спасибо,
Майк
Редактировать #1: Это в ответ на сообщение Томми Карлье:
Вот полный рабочий пример, который показывает, что с этим подходом могут сосуществовать как обработчики событий со строгой типизацией, так и текущие стандартные обработчики событий, которые используют параметр "отправитель объекта". Вы можете скопировать и вставить код и запустить его:
namespace csScrap.GenericEventHandling
{
class PublisherEventArgs : EventArgs
{
// ...
}
[SerializableAttribute]
public delegate void StrongTypedEventHandler<TSender, TEventArgs>(
TSender sender,
TEventArgs e
)
where TEventArgs : EventArgs;
class Publisher
{
public event StrongTypedEventHandler<Publisher, PublisherEventArgs> SomeEvent;
public void OnSomeEvent()
{
if (SomeEvent != null)
{
SomeEvent(this, new PublisherEventArgs());
}
}
}
class StrongTypedSubscriber
{
public void SomeEventHandler(Publisher sender, PublisherEventArgs e)
{
MessageBox.Show("StrongTypedSubscriber.SomeEventHandler called.");
}
}
class TraditionalSubscriber
{
public void SomeEventHandler(object sender, PublisherEventArgs e)
{
MessageBox.Show("TraditionalSubscriber.SomeEventHandler called.");
}
}
class Tester
{
public static void Main()
{
Publisher publisher = new Publisher();
StrongTypedSubscriber strongTypedSubscriber = new StrongTypedSubscriber();
TraditionalSubscriber traditionalSubscriber = new TraditionalSubscriber();
publisher.SomeEvent += strongTypedSubscriber.SomeEventHandler;
publisher.SomeEvent += traditionalSubscriber.SomeEventHandler;
publisher.OnSomeEvent();
}
}
}
Редактировать #2: Это в ответ на заявление Эндрю Хэра относительно ковариации и контравариантности и как это применяется здесь. Делегаты на языке C# так долго имели ковариацию и контравариантность, что это кажется "внутренним", но это не так. Это может быть даже то, что включено в CLR, я не знаю, но Visual Basic.NET не получал возможности ковариации и контравариантности для своих делегатов до.NET Framework 3.0 (VB.NET 2008). В результате Visual Basic.NET для.NET 2.0 и ниже не сможет использовать этот подход.
Например, приведенный выше пример можно перевести на VB.NET следующим образом:
Namespace GenericEventHandling
Class PublisherEventArgs
Inherits EventArgs
' ...
' ...
End Class
<SerializableAttribute()> _
Public Delegate Sub StrongTypedEventHandler(Of TSender, TEventArgs As EventArgs) _
(ByVal sender As TSender, ByVal e As TEventArgs)
Class Publisher
Public Event SomeEvent As StrongTypedEventHandler(Of Publisher, PublisherEventArgs)
Public Sub OnSomeEvent()
RaiseEvent SomeEvent(Me, New PublisherEventArgs)
End Sub
End Class
Class StrongTypedSubscriber
Public Sub SomeEventHandler(ByVal sender As Publisher, ByVal e As PublisherEventArgs)
MessageBox.Show("StrongTypedSubscriber.SomeEventHandler called.")
End Sub
End Class
Class TraditionalSubscriber
Public Sub SomeEventHandler(ByVal sender As Object, ByVal e As PublisherEventArgs)
MessageBox.Show("TraditionalSubscriber.SomeEventHandler called.")
End Sub
End Class
Class Tester
Public Shared Sub Main()
Dim publisher As Publisher = New Publisher
Dim strongTypedSubscriber As StrongTypedSubscriber = New StrongTypedSubscriber
Dim traditionalSubscriber As TraditionalSubscriber = New TraditionalSubscriber
AddHandler publisher.SomeEvent, AddressOf strongTypedSubscriber.SomeEventHandler
AddHandler publisher.SomeEvent, AddressOf traditionalSubscriber.SomeEventHandler
publisher.OnSomeEvent()
End Sub
End Class
End Namespace
VB.NET 2008 может работать на все 100%. Но сейчас я проверил его на VB.NET 2005, просто чтобы убедиться, и он не компилируется, заявив:
Метод 'Public Sub SomeEventHandler(отправитель как объект, e как vbGenericEventHandling.GenericEventHandling.PublisherEventArgs) "не имеет той же подписи, что и делегат" Delegate Sub StrongTypedEventHandler(из TSender, TEventArgs As System.EventArvent, издатель как издатель), отправитель как издатель "). '
По сути, делегаты инвариантны в версиях VB.NET 2005 и ниже. Я на самом деле думал об этой идее пару лет назад, но неспособность VB.NET справиться с этим беспокоила меня... Но я теперь твердо перешел на C#, и VB.NET теперь может справиться с этим, так что, ну, следовательно, эта почта.
Редактировать: Обновление № 3
Хорошо, я использую это довольно успешно в настоящее время. Это действительно хорошая система. Я решил назвать мой "StrongTypedEventHandler" как "GenericEventHandler", определенный следующим образом:
[SerializableAttribute]
public delegate void GenericEventHandler<TSender, TEventArgs>(
TSender sender,
TEventArgs e
)
where TEventArgs : EventArgs;
Помимо этого переименования, я реализовал его в точности так, как описано выше.
Он отключается по правилу FxCop CA1009, которое гласит:
"По соглашению, события.NET имеют два параметра, которые определяют отправителя и данные события. Подписи обработчика событий должны иметь следующую форму: void MyEventHandler(отправитель объекта, EventArgs e). Параметр" отправитель "всегда имеет тип System.Object, даже если возможно использовать более конкретный тип. Параметр 'e' всегда имеет тип System.EventArgs. События, которые не предоставляют данные о событиях, должны использовать тип делегата System.EventHandler. Обработчики событий возвращают void, чтобы они могли отправлять каждое событие для нескольких целевых методов. Любое значение, возвращаемое целевым объектом, будет потеряно после первого вызова. "
Конечно, мы все это знаем и все равно нарушаем правила. (Все обработчики событий могут использовать стандартный "объект-отправитель" в своей подписи, если они предпочтительнее в любом случае - это непреложное изменение.)
Так что использование SuppressMessageAttribute
делает трюк:
[SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly",
Justification = "Using strong-typed GenericEventHandler<TSender, TEventArgs> event handler pattern.")]
Я надеюсь, что этот подход станет стандартом в будущем. Это действительно работает очень хорошо.
Спасибо за все ваши мнения, ребята, я действительно ценю это...
Майк
11 ответов
Похоже, что Microsoft подхватила это, поскольку похожий пример теперь есть на MSDN:
То, что вы предлагаете, действительно имеет много смысла, и мне просто интересно, если это одна из тех вещей, которые просто так, потому что она была первоначально разработана до дженериков, или есть реальная причина для этого.
Среда выполнения Windows (WinRT) представляет TypedEventHandler<TSender, TResult>
делегат, который делает именно то, что ваш StrongTypedEventHandler<TSender, TResult>
делает, но, видимо, без ограничения на TResult
параметр типа:
public delegate void TypedEventHandler<TSender, TResult>(TSender sender,
TResult args);
Документация MSDN находится здесь.
Я взглянул на то, как это было сделано с новым WinRT и основано на других мнениях здесь, и наконец решил сделать это так:
[Serializable]
public delegate void TypedEventHandler<in TSender, in TEventArgs>(
TSender sender,
TEventArgs e
) where TEventArgs : EventArgs;
Это, кажется, лучший путь вперед, учитывая использование имени TypedEventHandler в WinRT.
Я согласен со следующими утверждениями:
- Я считаю, что более старые версии Visual Basic .NET до 2005 года не имеют делегированных ковариации и контравариантности.
- Я полностью понимаю, что это грани богохульства.
Прежде всего, ничто из того, что вы здесь сделали, не имеет ничего общего с ковариацией или контравариантностью. (Изменить: предыдущее утверждение неверно, для получения дополнительной информации см. Ковариантность и Контравариантность в делегатах). Это решение будет отлично работать во всех версиях CLR 2.0 и выше (очевидно, что это не будет работать в приложении CLR 1.0, так как оно использует обобщенные значения).
Во-вторых, я категорически не согласен с тем, что ваша идея граничит с "богохульством", поскольку это прекрасная идея.
Оглядываясь назад на богохульство как на единственную причину сделать отправителя типом объекта (если исключить проблемы с противоречивостью в коде VB 2005, который является ошибкой Microsoft IMHO), может кто-нибудь предложить хотя бы теоретический мотив для приближения второго аргумента к типу EventArgs. Если пойти еще дальше, есть ли веская причина для соответствия рекомендациям и соглашениям Microsoft в данном конкретном случае?
Необходимость разработки другой оболочки EventArgs для других данных, которые мы хотим передать внутри обработчика событий, кажется странной, почему нельзя передать эти данные напрямую. Рассмотрим следующие разделы кода
[Пример 1]
public delegate void ConnectionEventHandler(Server sender, Connection connection);
public partial class Server
{
protected virtual void OnClientConnected(Connection connection)
{
if (ClientConnected != null) ClientConnected(this, connection);
}
public event ConnectionEventHandler ClientConnected;
}
[Пример 2]
public delegate void ConnectionEventHandler(object sender, ConnectionEventArgs e);
public class ConnectionEventArgs : EventArgs
{
public Connection Connection { get; private set; }
public ConnectionEventArgs(Connection connection)
{
this.Connection = connection;
}
}
public partial class Server
{
protected virtual void OnClientConnected(Connection connection)
{
if (ClientConnected != null) ClientConnected(this, new ConnectionEventArgs(connection));
}
public event ConnectionEventHandler ClientConnected;
}
Насколько я понимаю, поле "Отправитель" всегда должно ссылаться на объект, который содержит подписку на событие. Если бы у меня были мои барабанщики, было бы также поле, содержащее информацию, достаточную для отмены подписки на событие, если оно стало необходимым (*) (рассмотрим, например, журнал изменений, который подписывается на события "коллекция изменена"; он состоит из двух частей, одна из которых выполняет реальную работу и содержит фактические данные, а другая предоставляет открытую оболочку интерфейса, основная часть может содержать слабую ссылку на часть оболочки. Если часть оболочки получает сборщик мусора, это будет означать больше никто не интересовался данными, которые собирались, и регистратор изменений должен, таким образом, отказаться от подписки на любое событие, которое он получает).
Поскольку возможно, что объект может отправлять события от имени другого объекта, я вижу некоторую потенциальную полезность для наличия поля "отправитель", имеющего тип Object, и для того, чтобы производное от EventArgs поле содержало ссылку на объект, который должен действовать на Однако бесполезное использование поля "отправитель", вероятно, ограничено тем, что у объекта нет чистого способа отписаться от неизвестного отправителя.
(*) На самом деле, более чистый способ обработки отписок состоит в том, чтобы иметь тип делегата многоадресной рассылки для функций, которые возвращают логическое значение; если функция, вызываемая таким делегатом, возвращает True, делегат будет исправлен для удаления этого объекта. Это будет означать, что делегаты больше не будут по-настоящему неизменными, но должно быть возможно осуществить такое изменение в поточно-ориентированном режиме (например, обнулить ссылку на объект и заставить код делегата многоадресной рассылки игнорировать любые встроенные ссылки на нулевые объекты). В этом сценарии попытка публикации и события для удаленного объекта может быть обработана очень аккуратно, независимо от того, откуда произошло событие.
Я думаю, что это отличная идея, и у MS может просто не быть времени или интереса, чтобы инвестировать в улучшение, как, например, когда они перешли из ArrayList в общие списки на основе.
В текущей ситуации (отправитель является объектом) вы можете легко прикрепить метод к нескольким событиям:
button.Click += ClickHandler;
label.Click += ClickHandler;
void ClickHandler(object sender, EventArgs e) { ... }
Если отправитель будет универсальным, цель события click будет иметь не тип Button или Label, а тип Control (поскольку событие определено в Control). Таким образом, некоторые события в классе Button будут иметь цель типа Control, другие будут иметь другие типы целей.
Я не думаю, что есть что-то не так с тем, что вы хотите сделать. По большей части я подозреваю, что object sender
параметр остается для того, чтобы продолжать поддерживать код до 2.0.
Если вы действительно хотите внести это изменение для общедоступного API, вы можете рассмотреть возможность создания собственного базового класса EvenArgs. Что-то вроде этого:
public class DataEventArgs<TSender, TData> : EventArgs
{
private readonly TSender sender, TData data;
public DataEventArgs(TSender sender, TData data)
{
this.sender = sender;
this.data = data;
}
public TSender Sender { get { return sender; } }
public TData Data { get { return data; } }
}
Тогда вы можете объявить свои события, как это
public event EventHandler<DataEventArgs<MyClass, int>> SomeIndexSelected;
И такие методы:
private void HandleSomething(object sender, EventArgs e)
все еще сможет подписаться.
РЕДАКТИРОВАТЬ
Эта последняя строка заставила меня задуматься... На самом деле вы должны иметь возможность реализовать то, что вы предлагаете, без нарушения каких-либо внешних функций, поскольку у среды выполнения нет проблем с понижением параметров. Я все еще склоняюсь к DataEventArgs
решение (лично). Я бы сделал это, однако, зная, что это избыточно, поскольку отправитель хранится в первом параметре и как свойство аргументов события.
Одно преимущество придерживаться с DataEventArgs
заключается в том, что вы можете связывать события, изменяя отправителя (для представления последнего отправителя), в то время как EventArgs сохраняет первоначального отправителя.
Действуй. Для некомпонентного кода я часто упрощаю подписи событий, чтобы
public event Action<MyEventType> EventName
где MyEventType
не наследуется от EventArgs
, Зачем беспокоиться, если я никогда не собираюсь использовать кого-либо из членов EventArgs.