Подпись события in.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;

это не все, что отличается от действия, но, используя 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" как "object" для обработки полной ширины потенциальных объектов-отправителей.

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

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

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

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

вот некоторые ссылки, которые я найдено:

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

(2) C# простое повышение события-использование "отправителя" против пользовательских EventArgs [StackOverflow 2009]

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

меня интересует чье-либо и всеобщее мнение по этому поводу...

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

Майк

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

Edit #2: это в ответ на заявление Андрея Зайца относительно ковариации и контравариантности и как это применяется здесь. Делегаты на языке C# имеют ковариацию и контравариантность так долго, что он просто чувствует себя "внутренним", но это не так. Это может быть даже то, что включено в среде CLR, я не знаю, но Visual Basic .NET не получил возможности ковариации и контравариации для своих делегатов до .NET Framework 3.0 (VB.NET 2008). И как результат, визуальный 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.Дженерикевентхандлинг.PublisherEventArgs)' не имеет такой же подписи, как суб делегата делегат StrongTypedEventHandler(Of TSender, TEventArgs Как Системы.EventArgs) (отправитель Как Издатель, как 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). Параметр 'отправитель' всегда имеет тип системы.Объект, даже если это возможно, чтобы использовать более конкретный тип. Параметр ' e ' является всегда типа системы.EventArgs. События, которые не предоставляют данные о событиях следует использовать систему.EventHandler в тип делегата. Обработчики событий возвращаются void, чтобы они могли отправлять каждое событие для нескольких целевых методов. Любое значение возвращенный целью будет потерян после первого звонка."

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

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

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

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

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

Майк

11 104

11 ответов:

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

Универсальные Делегаты

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

имея необходимость разработать другую оболочку 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;
}

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

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

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

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

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

будет еще можно будет подписаться.

EDIT

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

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

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

public event Action<MyEventType> EventName

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