Подпись события в .NET - Использование строго типизированного «отправителя»? - PullRequest
105 голосов
/ 26 июня 2009

Я полностью понимаю, что то, что я предлагаю, не соответствует рекомендациям .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]

(2) C # simple Event Raising - использование «отправителя» и пользовательских EventArgs [StackOverflow 2009]

(3) Шаблон подписи события в .net [StackOverflow 2008]

Меня интересует мнение всех и каждого по этому поводу ...

Заранее спасибо,

Mike

Редактировать # 1: Это ответ на сообщение Томми Карлира :

Вот полный рабочий пример, который показывает, что с этим подходом могут сосуществовать как обработчики событий со строгой типизацией, так и текущие стандартные обработчики событий, которые используют параметр 'object sender' Вы можете скопировать и вставить код и запустить его:

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();
        }
    }
}

Edit # 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)» не имеет той же подписи, что и делегат 'Делегат Sub StrongTypedEventHandler (Of TSender, TEventArgs As System.EventArgs) (отправитель As Publisher, e As PublisherEventArgs) '

По сути, делегаты инвариантны в версиях 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, даже если можно нанять больше конкретный тип. Параметр «е» всегда типа System.EventArgs. События, которые не предоставляют данные о событиях следует использовать System.EventHandler тип делегата. Возвращаются обработчики событий недействительными, чтобы они могли отправлять каждое событие к нескольким целевым методам. Любое значение возвращенный целью будет потерян после первого звонка. "

Конечно, мы все это знаем и все равно нарушаем правила. (Все обработчики событий могут использовать стандартный «объект-отправитель» в своей подписи, если они предпочтительнее, в любом случае - это непрерывное изменение.)

Так что использование SuppressMessageAttribute делает свое дело:

[SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly",
    Justification = "Using strong-typed GenericEventHandler<TSender, TEventArgs> event handler pattern.")]

Я надеюсь, что этот подход станет стандартом в какой-то момент в будущем. Это действительно работает очень хорошо.

Спасибо за ваше мнение, ребята, я действительно ценю это ...

Mike

Ответы [ 11 ]

25 голосов
/ 25 ноября 2010

Похоже, Microsoft подобрал это, так как похожий пример теперь на MSDN:

Общие делегаты

13 голосов
/ 26 июня 2009

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

12 голосов
/ 15 марта 2012

Среда выполнения Windows (WinRT) представляет делегат TypedEventHandler<TSender, TResult>, который делает именно то, что делает ваш StrongTypedEventHandler<TSender, TResult>, но без ограничения на параметр типа TResult:

public delegate void TypedEventHandler<TSender, TResult>(TSender sender,
                                                         TResult args);

Документация MSDN здесь .

5 голосов
/ 07 июля 2012

Я взглянул на то, как это было сделано с новым WinRT и основано на других мнениях здесь, и наконец решил сделать это так:

[Serializable]
public delegate void TypedEventHandler<in TSender, in TEventArgs>(
    TSender sender,
    TEventArgs e
) where TEventArgs : EventArgs;

Это, кажется, лучший способ продвижения вперед, учитывая использование имени TypedEventHandler в WinRT.

5 голосов
/ 26 июня 2009

Я согласен со следующими утверждениями:

  • Я полагаю, что более старые версии Visual Basic .NET до 2005 года не имеют ковариации и контравариантности делегатов.
  • Я полностью осознаю, что это грани богохульства.

Прежде всего, ничто из того, что вы здесь сделали, не имеет ничего общего с ковариацией или контравариантностью. ( Редактировать: Предыдущее утверждение неверно, для получения дополнительной информации см. Ковариантность и контравариантность в делегатах ) Это решение будет прекрасно работать во всех версиях CLR 2.0 и выше (очевидно, что не будет работать в приложении CLR 1.0, так как оно использует обобщенные значения) ,

Во-вторых, я категорически не согласен с тем, что ваша идея граничит с «богохульством», поскольку это прекрасная идея.

2 голосов
/ 11 ноября 2010

Оглядываясь назад на богохульство как на единственную причину превращения отправителя в тип объекта (если исключить проблемы с противоречивостью в коде VB 2005, который является ошибкой Microsoft ИМХО), может кто-нибудь предложить хотя бы теоретический мотив прибить второй аргумент Тип 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;
}
2 голосов
/ 31 октября 2010

Насколько я понимаю, поле «Отправитель» всегда должно относиться к объекту, который содержит подписку на событие. Если бы у меня были мои барабанщики, было бы также поле, содержащее информацию, достаточную для отмены подписки на событие, если оно стало необходимым (*) (рассмотрим, например, журнал изменений, который подписывается на события «коллекция изменена»; он состоит из двух частей одна из которых выполняет реальную работу и содержит фактические данные, а другая обеспечивает оболочку открытого интерфейса, основная часть может содержать слабую ссылку на часть оболочки. Если часть оболочки получает сборщик мусора, это будет означать больше никто не интересовался данными, которые собирались, и поэтому регистратор изменений должен отписываться от любого получаемого события).

Поскольку вполне возможно, что объект может отправлять события от имени другого объекта, я вижу некоторую потенциальную полезность для наличия поля «отправитель», имеющего тип объекта, и для того, чтобы производное от EventArgs поле содержало ссылку на объект, на который следует воздействовать. Однако бесполезное использование поля «отправитель», вероятно, ограничено тем, что у объекта нет чистого способа отписаться от неизвестного отправителя.

(*) На самом деле, более чистый способ обработки отписок - иметь тип делегата многоадресной рассылки для функций, возвращающих Boolean; если функция, вызываемая таким делегатом, возвращает True, делегат будет исправлен для удаления этого объекта. Это будет означать, что делегаты больше не будут по-настоящему неизменными, но должна быть возможность осуществить такое изменение в поточно-ориентированном режиме (например, путем обнуления ссылки на объект и с помощью того, что код делегата многоадресной рассылки игнорирует любые встроенные нулевые ссылки на объекты). При таком сценарии попытка публикации и события для удаленного объекта может быть обработана очень аккуратно, независимо от того, откуда произошло событие.

2 голосов
/ 26 июня 2009

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

1 голос
/ 26 июня 2009

Перейти на это. Для некомпонентного кода я часто упрощаю сигнатуры событий просто:

public event Action<MyEventType> EventName

, где MyEventType не наследуется от EventArgs. Зачем беспокоиться, если я никогда не собираюсь использовать кого-либо из членов EventArgs.

1 голос
/ 26 июня 2009

В текущей ситуации (отправитель является объектом) вы можете легко прикрепить метод к нескольким событиям:

button.Click += ClickHandler;
label.Click += ClickHandler;

void ClickHandler(object sender, EventArgs e) { ... }

Если отправитель будет универсальным, цель события click будет не типа Button или Label, а типа Control (поскольку событие определено в Control). Поэтому некоторые события в классе Button будут иметь цель типа Control, а другие - другие типы целей.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...