Подпись к событию в .NET. Использование сильного напечатанного "Отправителя"?

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

(1) Должен ли я использовать это для моей собственной работы по разработке, что составляет 100% для внутренних целей.

(2) Является ли это концепцией, которую дизайнеры фреймворка могли рассмотреть или изменить?

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

class Publisher
{
    public event EventHandler<PublisherEventArgs> SomeEvent;
}

Я рассматриваю использование сигнатуры события, которая использует параметр "sender" с сильным типом, следующим образом:

Сначала определите "StrongTypedEventHandler":

[SerializableAttribute]
public delegate void StrongTypedEventHandler<TSender, TEventArgs>(
    TSender sender,
    TEventArgs e
)
where TEventArgs : EventArgs;

Это не все, что отличается от Action < TSender, TEventArgs > , но, используя 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")
        {
            // ...
        }
    }
}

То есть, если обработчик события должен был подписаться на события из разрозненных (или, возможно, неизвестных) типов объектов, обработчик мог бы ввести параметр "sender" как "объект", чтобы обрабатывать всю ширину потенциальных объектов отправителя.

Помимо нарушения соглашения (что-то, что я не воспринимаю легко, поверьте мне), я не могу придумать никаких недостатков.

Здесь могут быть некоторые проблемы с соблюдением CLS. Это выполняется в Visual Basic.NET 2008 на 100% отлично (я тестировал), но я считаю, что более старые версии Visual Basic.NET до 2005 года не имеют ковариации и контравариантности делегатов. [Изменить: с тех пор я тестировал это, и это подтверждено: VB.NET 2005 и ниже не могут справиться с этим, но VB.NET 2008 на 100% лучше. См. "Редактировать # 2" ниже.] Могут быть и другие языки .NET, у которых также есть проблемы с этим, я не могу быть уверен.

Но я не вижу, что я разрабатываю для любого языка, отличного от С# или Visual Basic.NET, и я не против ограничивать его на С# и VB.NET для .NET Framework 3.0 и выше. (Я не мог себе представить, чтобы вернуться к версии 2.0 на данный момент, если честно.)

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

Вот некоторые связанные ссылки, которые я нашел:

(1) Руководство по дизайну событий [MSDN 3.5]

(2) qaru.site/info/6309/...

(3) qaru.site/info/6310/...

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

Спасибо заранее,

Mike

Изменить # 1: Это ответ на сообщение Tommy Carlier:

Здесь приведен полный рабочий пример, который показывает, что как сильные типизированные обработчики событий, так и текущие стандартные обработчики событий, которые используют параметр "отправитель объекта", могут сосуществовать с этим подходом. Вы можете скопировать-вставить в код и запустить его:

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: Это ответ на выражение Эндрю Харе относительно ковариации и контравариантности и того, как это применимо здесь. Делегаты на языке С# имели ковариацию и контравариантность так долго, что они просто чувствовали себя "неотъемлемыми", но это не так. Возможно, это даже может быть что-то, что включено в 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)" не имеет такой же делегат ' StrongTypedEventHandler (из TSender, TEventArgs As System.EventArgs) (отправитель Как издатель, e As PublisherEventArgs)

В принципе, делегаты являются инвариантными в версиях VB.NET 2005 и ниже. Я действительно думал об этой идее пару лет назад, но неспособность VB.NET справиться с этим беспокоила меня... Но я теперь полностью перешел на С#, и 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). Параметр 'sender' всегда имеет тип System.Object, даже если можно использовать более определенного типа. Параметр 'e' всегда типа System.EventArgs. События, которые не предоставляют данные о событиях следует использовать System.EventHandler тип делегата. Обработчики событий возвращаются void, чтобы они могли отправлять каждое событие к нескольким целевым методам. Любое значение возвращенный мишенью, будет потерян после первого вызова.

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

Таким образом, использование SuppressMessageAttribute делает трюк:

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

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

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

Mike

Ответы

Ответ 1

Кажется, Microsoft подобрала это, поскольку аналогичный пример теперь находится в MSDN:

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

Ответ 2

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

Ответ 3

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

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

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

Ответ 4

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

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

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

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

Ответ 5

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

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

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

Ответ 6

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

Ответ 7

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

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

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

Ответ 8

Оглядываясь назад на богохульство как единственную причину, по которой отправитель создает тип объекта (если опустить проблемы с контравариантностью в коде 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;
}

Ответ 9

Я не думаю, что что-то не так с тем, что вы хотите сделать. По большей части, я подозреваю, что параметр object sender остается для продолжения поддержки кода pre 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)

все равно сможет подписаться.

ИЗМЕНИТЬ

Эта последняя строка заставила меня немного подумать... Фактически вы должны реализовать то, что вы предлагаете, не нарушая никаких внешних функций, поскольку среда выполнения не имеет проблем с параметрами downcasting. Я все равно склоняюсь к решению DataEventArgs (лично). Я бы сделал это, однако зная, что он избыточен, поскольку отправитель хранится в первом параметре и как свойство аргументов события.

Одно из преимуществ привязки к DataEventArgs заключается в том, что вы можете связывать события, меняя отправителя (для представления последнего отправителя), в то время как EventArg сохраняет исходный отправитель.

Ответ 10

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

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

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

Если отправитель будет общим, цель события click не будет иметь тип Button или Label, а тип Control (потому что событие определено в Control). Таким образом, некоторые события в классе Button будут иметь целевое управление типа, другие будут иметь другие целевые типы.

Ответ 11

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

public event Action<MyEventType> EventName

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