Вопрос по c#, .net, vb.net, events – Подпись события в .NET - Использование строго типизированного «отправителя»?

104

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

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

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

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

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

Я рассматриваю возможность использования подписи события, в которой используется строго типизированный «отправитель»; параметр, следующим образом:

Сначала определите & quot; StrongTypedEventHandler & quot ;:

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

Это не все, что отличается от действия & lt; TSender, TEventArgs & gt ;, но с использованием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 не имеют делегированных ковариации и контравариантности.[Edit: I have since tested this, and it is confirmed: VB.NET 2005 and below cannot handle this, but VB.NET 2008 is 100% fine. See "Edit #2", below.] Могут быть другие языки .NET, у которых также есть проблема с этим, я не могу быть уверен.

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

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

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

(1) Руководство по разработке мероприятий [MSDN 3.5]

(2) C # simple Event Raising - использование & # x201C; sender & # x201D; против пользовательских 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). В результате 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, просто чтобы убедиться, и он не компилируется, заявив:

Method 'Public Sub SomeEventHandler(sender As Object, e As vbGenericEventHandling.GenericEventHandling.PublisherEventArgs)' does not have the same signature as delegate 'Delegate Sub StrongTypedEventHandler(Of TSender, TEventArgs As System.EventArgs)(sender As Publisher, e As PublisherEventArgs)'

По сути, делегаты инвариантны в версиях VB.NET 2005 и ниже. Я на самом деле думал об этой идее пару лет назад, но неспособность VB.NET справиться с этим беспокоила меня ... Но я сейчас твердо перешел на C #, и VB.NET теперь может справиться с этим, так что отсюда и этот пост.

Edit: Update #3

Хорошо, я использую это довольно успешно в настоящее время. Это действительно хорошая система. Я решил назвать мой «StrongTypedEventHandler» как "GenericEventHandler", определяется следующим образом:

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

Помимо этого переименования, я реализовал его точно так, как описано выше.

Он отключается по правилу FxCop CA1009, которое гласит:

"By convention, .NET events have two parameters that specify the event sender and event data. Event handler signatures should follow this form: void MyEventHandler( object sender, EventArgs e). The 'sender' parameter is always of type System.Object, even if it is possible to employ a more specific type. The 'e' parameter is always of type System.EventArgs. Events that do not provide event data should use the System.EventHandler delegate type. Event handlers return void so that they can send each event to multiple target methods. Any value returned by a target would be lost after the first call."

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

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

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

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

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

Майк

Чувак, я хотел бы, чтобы мой проект сделал это с самого начала, я ненавижу кастинг отправителя. Matt H
Мои аргументы на самом деле не были направлены на вас: конечно, вы должны делать это в своих собственных проектах. Они были аргументами, почему это не могло работать в BCL. Tommy Carlier
Сделай это. (Не думаю, что это оправдывает ответ.) Konrad Rudolph
Еще одно предложение, просто назовите егоEventHandler<,> чемGenericEventHandler<,>, Там уже дженерикEventHandler<> в BCL, который называется просто EventHandler. Таким образом, EventHandler является более распространенным именем, и делегаты поддерживают перегрузки типов. nawfal
СейчасTHIS это вопрос. Видите, ребята? Ни один из этих твитов размером сoh hi this my hom work solve it plz :code dump: вопросы, но вопрос, который мыlearn from. Camilo Martin

Ваш Ответ

11   ответов
2

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

Возможно, вы правы ... С другой стороны, я думаю, что это просто "стандарт" и, возможно, вообще не техническая проблема. То есть эта возможность может присутствовать во всех современных языках .NET, я не знаю. Я знаю, что C # и VB.NET могут справиться с этим. Однако я не уверен, насколько широко это работает во всех современных языках .NET ... Но так как это работает в C # и VB.NET, и все здесь поддерживают это, я думаю, что я, скорее всего, сделаю это. :-) Mike Rosenblum
2

чтобы сделать отправителя типом объекта (если исключить проблемы с противоречивостью в коде VB 2005, который является ошибкой IMHO от 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;
}
Ему даже не нужно наследовать его, вы можете просто добавить дополнительную функциональность непосредственно в ваш класс args события, и он продолжит нормально работать. Тем не менее, ограничение прикрепления аргументов к eventargs было удалено, потому что это не имело большого смысла для многих сценариев, т.е. когда вы знаете, что вам никогда не понадобится расширять функциональность определенного события, или когда все, что вам нужно, это аргумент типа значения в приложениях, очень чувствительных к производительности.
Однако, наследуя от EventArgs, будущая версия может наследовать от вашего более старого класса аргументов события и дополнять его. Все старые вызывающие могут по-прежнему работать точно так, как есть, работая с базовым классом вашего нового класса аргументов события. Очень чистый. Больше работы для вас, но чище для любых абонентов, которые зависят от вашей библиотеки. Mike Rosenblum
Да, создание отдельного класса, который наследуется от System.EventArgs, может показаться неинтуитивным и представляет собой дополнительную работу, но для этого есть очень веские причины. Если вам никогда не нужно менять код, тогда ваш подход хорош. Но реальность такова, что вам может потребоваться увеличить функциональность события в будущей версии и добавить свойства в аргументы события. В вашем сценарии вам придется добавить дополнительные перегрузки или необязательные параметры к сигнатуре обработчика событий. Это подход, используемый в VBA и устаревшей версии VB 6.0, который работает, но на практике немного уродлив. Mike Rosenblum
1

чтобы

public event Action<MyEventType> EventName

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

+1, я тоже этим пользуюсь. Иногда простота побеждает! Или дажеevent Action<S, T>, event Action<R, S, T> и т.д. у меня есть метод расширенияRaise их поток-безопасно :)
Согласен! Почему мы должны чувствовать себя обезьянами?
5

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

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

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

Зачем добавлять общее ограничение для TEventArgs? Он был удален из EventHandler & lt; & gt; и TypedEventHandler & lt;, & gt; потому что это не имело смысла.
13

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

Я уверен, что именно в этом причина. Тем не менее, теперь, когда более новые версии языка имеют контравариантность для обработки этого, похоже, что они должны иметь возможность обрабатывать это обратно-совместимым образом. Предыдущие обработчики, которые используют «объект-отправитель»; не сломался бы. Но это не так для более старых языков и может быть не так для некоторых современных языков .NET, я не уверен. Mike Rosenblum
2

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

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

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

Я полностью согласен с вами
25

Microsoft воспользовалась этим, поскольку похожий пример теперь есть в MSDN:

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

+1 Ах, отлично. Они действительно это поняли. Это хорошо. Тем не менее, я надеюсь, что они делают это признанным шаблоном в VS IDE, потому что, как сейчас, более неудобно использовать этот шаблон в терминах IntelliSense и т. Д. Mike Rosenblum
1

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

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

EDIT

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

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

Привет, Майкл, это довольно аккуратная альтернатива. Мне это нравится. Как вы упомянули, тем не менее, иметь «отправителя» излишне параметр эффективно передан в два раза. Подобный подход обсуждается здесь:stackoverflow.com/questions/809609/…и консенсус кажется, что это слишком нестандартно. Вот почему я не решался предложить "типизированный" отправитель ". Идея здесь. (Кажется, что это было хорошо принято, поэтому я доволен.) Mike Rosenblum
12

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

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

Документация MSDNВот.

Это может быть недосмотр со стороны команды разработчиков; кто знает.
Это не похоже на недосмотр. Ограничение было удалено из System.EventHandler & lt; TEventArgs & gt; делегат также.referencesource.microsoft.com/#mscorlib/system/…
ну, события работают нормально без использованияEventArgsэто просто условность
В документации TypedEventHandler конкретно говорится, чтоargs будетnull если нет данных о событиях, то кажется, что они по умолчанию отказываются от использования по существу пустого объекта. Я предполагаю, что первоначальная идея заключалась в том, чтобы создать метод со вторым параметром типаEventArgs может обработать любое событие, потому что типы всегда будут совместимы. Теперь они, вероятно, понимают, что возможность обрабатывать несколько разных событий одним методом не так уж важна.
5

I believe that the older versions of Visual Basic .NET through 2005 do not have delegate covariance and contravariance. I do fully realize that this verges on blasphemy.

First of all, nothing you have done here has anything to do with covariance or contravariance. (Edit: The previous statement is wrong, for more information please see Ковариантность и контравариантность в делегатах) Это решение будет отлично работать во всех версиях CLR 2.0 и выше (очевидно, этоnot работать в приложении CLR 1.0, так как оно использует дженерики).

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

@Mike - всегда интересно узнать, что CLR поддерживает, но не поддерживается ни в одном из языков .NET.
@ Майк - мои извинения, вы на 100% правы! Я отредактировал свой ответ, чтобы отразить вашу точку зрения :)
Ах, интересно! Кажется, что ковариация / контравариантность делегата является частью CLR, но (по причинам, которые я не знаю), она не была раскрыта VB.NET до самой последней версии. Вот статья Франческо Балена, в которой показано, как можно добиться дисперсии делегата с помощью Reflection, если она не включена самим языком:dotnet2themax.com/blogs/fbalena/…. Mike Rosenblum
Привет Андрей, спасибо за большие пальцы! Учитывая уровень вашей репутации, это действительно много значит для меня ... В вопросе ковариации / контравариантности: если делегат, предоставленный подписчиком, точно не совпадает с подписью события издателя, то участвуют ковариация и контрвариантность. В C # всегда были ковариация и контравариантность делегата, поэтому он кажется внутренним, но VB.NET не имел ковариации и контравариантности делегата до .NET 3.0. Поэтому VB.NET для .NET 2.0 и ниже не сможет использовать эту систему. (См. Пример кода, который я добавил в «Edit # 2», выше.) Mike Rosenblum
1

крепить метод к нескольким событиям:

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

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

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

Томми, ты можешь сделатьexactly то же самое с системой, которую я предлагаю. Вы по-прежнему можете использовать стандартный обработчик событий, который имеет «отправитель объекта»; параметр для обработки этих строго типизированных событий. (См. Пример кода, который я сейчас добавил к исходному сообщению.) Mike Rosenblum
Да, я согласен, это хорошая вещь в стандартных событиях .NET, принято!

Похожие вопросы