web-dev-qa-db-ja.com

.NETのイベントシグネチャ-厳密に型指定された「送信者」を使用していますか?

私が提案しているものは.NETガイドラインに準拠していないことを十分に理解しているため、おそらくこの理由だけでは不十分な考えです。ただし、次の2つの観点からこれを検討したいと思います。

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

これは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")
        {
            // ...
        }
    }
}

つまり、イベントハンドラーが異種の(またはおそらく不明な)オブジェクトタイプからイベントをサブスクライブする必要がある場合、ハンドラーは、潜在的な送信者オブジェクトの全範囲を処理するために、「送信者」パラメーターを「オブジェクト」として入力できます。

慣習を破る以外(これは私が軽んじることではないので、私を信じてください)以外に、これの欠点は考えられません。

ここにいくつかのCLS準拠問題があるかもしれません。これはVisual Basic .NET 2008で100%正常に動作します(テストしました)が、2005までの古いバージョンのVisual Basic .NETにはデリゲートの共分散と反分散がないと思います。 [編集:これをテストしてから確認しました。VB.NET2005以前ではこれを処理できませんが、VB.NET 2008は100%正常です。下記の「編集#2」を参照してください。]これに問題がある他の.NET言語が存在する可能性があります。確信が持てません。

しかし、私はC#またはVisual Basic .NET以外の言語で開発しているようには見えません。また、.NET Framework 3.0以降のC#およびVB.NETに制限することを気にしません。 (正直に言うと、この時点で2.0に戻ることは想像できませんでした。)

他の誰かがこれの問題について考えることができますか?それとも、これは単に慣習で破られて、人々の腹を回すだけですか?

ここに私が見つけたいくつかの関連リンクがあります:

(1) イベント設計ガイドライン[MSDN 3.5]

(2) C#シンプルイベント発生-「送信者」とカスタムEventArgsの使用[StackOverflow 2009]

(3) 。netのイベント署名パターン[StackOverflow 2008]

私はこれについて誰かの意見や誰の意見にも興味があります...

前もって感謝します、

マイク

Edit#1:これは Tommy Carlierの投稿 への応答です:

以下は、厳密に型指定されたイベントハンドラーと、 '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();
        }
    }
}

編集#2:これは、共分散および反変に関する Andrew Hareのステートメント への応答であり、ここでの適用方法です。 C#言語のデリゲートには共分散と反変があり、「内在的」と感じるだけですが、そうではありません。 CLRで有効になっているものかもしれませんが、Visual Basic .NETは、.NET Framework 3.0(VB.NET 2008)まで、デリゲートの共分散および反変の機能を備えていませんでした。その結果、.NET 2.0以前のVisual Basic.NETはこのアプローチを利用できません。

たとえば、上記の例は次のように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(sender As Object、e As vbGenericEventHandling.GenericEventHandling.PublisherEventArgs)'には、デリゲート 'Delegate Sub StrongTypedEventHandler(Of TSender、TEventArgs As System.EventArgs)(sender 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イベントには、イベントの送信者とイベントデータを指定する2つのパラメーターがあります。イベントハンドラーのシグネチャは、次の形式に従う必要があります:void MyEventHandler(object sender、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.")]

このアプローチが将来の標準になることを願っています。それは本当にとてもうまくいきます。

みなさん、ありがとうございました。本当に感謝しています...

マイク

105
Mike Rosenblum

同様の例が現在MSDNにあるので、Microsoftはこれを取り上げているようです。

ジェネリックデリゲート

25
Bas

あなたが提案していることは実際に多くの意味を成します、そしてこれが単にジェネリックの前に最初に設計されたのでそれが単純にそういうものの1つであるのか、これに本当の理由があるのか​​と思います。

13
BFree

Windowsランタイム(WinRT)はTypedEventHandler<TSender, TResult>デリゲート。これはStrongTypedEventHandler<TSender, TResult>は実行しますが、TResult型パラメーターに制約がないようです:

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

MSDNドキュメントは here です。

13
Pierre Arnaud

私は次の声明について問題を取り上げます。

  • 2005年までのVisual Basic .NETの古いバージョンには、デリゲートの共分散と逆分散がないと思います。
  • 私はこれが冒とくに近づいていることを完全に理解しています。

まず、ここで行ったことは共分散または反変とは何の関係もありません。編集:前のステートメントは間違っています。詳細については、参照してください デリゲートの共分散と反変 =)このソリューションは、すべてのCLRバージョン2.0以降で正常に機能します(明らかに、ジェネリックを使用するため、CLR 1.0アプリケーションでは機能しません)。

第二に、私はあなたのアイデアが「冒とく」に集中することには強く反対します。これは素晴らしいアイデアだからです。

5
Andrew Hare

私はこれが新しいWinRTでどのように処理されるかをここで他の意見に基づいてのぞいて、最後に次のようにすることで解決しました。

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

これは、WinRTでTypedEventHandlerという名前を使用することを考えると、最善の方法と思われます。

5
Inverness

私はそれは素晴らしいアイデアだと思います。たとえば、MSがArrayListからジェネリックベースのリストに移行したときなど、MSはこれを改善するために投資する時間や関心がないかもしれません。

2
Otávio Décio

私の理解から、「送信者」フィールドは常にイベントサブスクリプションを保持するオブジェクトを参照することになっています。もし私がdrutherを持っているなら、それが必要になった場合にイベントをサブスクライブ解除するのに十分な情報を保持するフィールドもあります(*)(たとえば、「collection-changed」イベントにサブスクライブする変更ロガーを考えてください;これは2つの部分を含みますの1つは実際の作業を行い、実際のデータを保持し、もう1つはパブリックインターフェイスラッパーを提供します。メインパーツはラッパーパーツへの弱い参照を保持できます。ラッパーパーツがガベージコレクションされると、それは収集されているデータに関心のある人がいなくなったため、変更ロガーは受け取ったイベントの登録を解除する必要があります)。

オブジェクトが別のオブジェクトに代わってイベントを送信する可能性があるため、オブジェクトタイプの「送信者」フィールドを使用し、EventArgs派生フィールドにオブジェクトへの参照を含めることで、行動する。ただし、「sender」フィールドの有用性は、オブジェクトが不明な送信者からサブスクライブを解除するための明確な方法がないという事実によっておそらく制限されます。

(*)実際には、サブスクリプション解除を処理するより明確な方法は、ブール値を返す関数のマルチキャストデリゲートタイプを持つことです。そのようなデリゲートによって呼び出された関数がTrueを返す場合、デリゲートはそのオブジェクトを削除するようにパッチされます。これは、デリゲートが真に不変ではなくなることを意味しますが、そのような変更をスレッドセーフな方法で実行できるようにする必要があります(たとえば、オブジェクト参照をnullにし、マルチキャストデリゲートコードに埋め込まれたnullオブジェクト参照を無視させる)。このシナリオでは、イベントの発生元に関係なく、破棄されたオブジェクトへのパブリッシュおよびイベントの試行は非常にクリーンに処理できました。

2
supercat

冒涜を送信者をオブジェクトタイプにする唯一の理由として振り返ります(VB 2005コード、Microsoftの誤解である2005コードでの矛盾の問題を省略する場合)、誰でも少なくとも理論的な動機を示唆できますEventArgsタイプの2番目の引数を釘付けにしたため、さらに進んで、この特定のケースで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
Lu4

頑張れ。非コンポーネントベースのコードの場合、イベントシグネチャを単純化して

public event Action<MyEventType> EventName

ここで、MyEventTypeEventArgsを継承しません。なぜわざわざ、EventArgsのメンバーを使用するつもりがない場合。

1
Scott Weinstein

あなたがやりたいことに問題はないと思います。ほとんどの場合、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を使用する利点の1つは、EventArgsが元の送信者を保持しながら、イベントをチェーンし、送信者を変更して(最後の送信者を表す)ことができることです。

1
Michael Meadows

現在の状況(送信者はオブジェクト)では、メソッドを複数のイベントに簡単にアタッチできます。

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

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

送信者がジェネリックの場合、クリックイベントのターゲットはボタンまたはラベルタイプではなく、コントロールタイプになります(イベントがコントロールで定義されているため)。したがって、Buttonクラスの一部のイベントには、コントロールタイプのターゲットがあり、他のイベントには他のターゲットタイプがあります。

1
Tommy Carlier