.NETのイベント署名—厳密に型指定された「送信者」を使用していますか?


106

私が提案しているものは.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]

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

前もって感謝します、

マイク

編集#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:これは、共分散と反変に関するAndrew Hareの声明、およびここでの適用方法に対応しています。C#言語のデリゲートには共分散と反変があり、「内在的」と感じるだけですが、そうではありません。CLRで有効になっているものかもしれませんが、Visual Basic .NETは、.NET Framework 3.0(VB.NET 2008)まで、デリゲートの共分散機能と反変機能を備えていませんでした。その結果、Visual Basic.NET for .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(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)。より具体的なタイプを使用することも可能です。「e」パラメーターは常にタイプSystem.EventArgsです。イベントデータを提供しないイベントは、System.EventHandlerデリゲートタイプを使用する必要があります。イベントハンドラーはvoidを返して送信できるようにします各イベントを複数のターゲットメソッドに送信します。ターゲットから返された値は、最初の呼び出し後に失われます。」

もちろん、私たちはこれらすべてを知っており、とにかくルールを破っています。(いずれの場合でも、すべてのイベントハンドラーは、必要に応じて、標準の「オブジェクト送信者」をシグネチャで使用できます。これは、互換性を保つための変更です。)

したがって、aを使用するとSuppressMessageAttributeうまくいきます。

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

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

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

マイク


6
やれ。(これが答えを正当化するとは思わないでください。)
Konrad Rudolph

1
私の議論は実際にはあなたに向けられていませんでした:もちろんあなたはあなた自身のプロジェクトでこれをするべきです。それらは、BCLで機能しない可能性がある理由です。
Tommy Carlier、2009年

3
男、私は私のプロジェクトが最初からこれをやっていたらいいのに、私は送信者をキャストするのが嫌いです。
マットH

7
今、これが問題です。ほらね?これらのつぶやきサイズのoh hi this my hom work solve it plz :code dump:質問の1つではなく、私たちから学んだ質問です。
Camilo Martin

3
もう一つの提案は、ちょうどそれに名前を付けるEventHandler<,>よりもGenericEventHandler<,>EventHandler<>EventHandlerという名前のBCLにはすでにジェネリックがあります。したがって、EventHandlerはより一般的な名前であり、デリゲートは型のオーバーロードをサポートしています
nawfal

回答:


25

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

ジェネリックデリゲート


2
+1ああ、すばらしい。彼らは確かにこれを手に入れました。これはいい。それが今であるとして、などのIntelliSense、という点で、このパターンを使用することがより厄介である、ので、私は、彼らがVS IDE内でこの認識されたパターンを作ること、しかし、願っています
マイク・ローゼンブラム

13

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


1
これがまさにその理由だと私は確信しています。ただし、新しいバージョンの言語ではこれを処理するために反変があるため、下位互換性のある方法でこれを処理できるはずです。「送信者オブジェクト」を使用する以前のハンドラは壊れませんでした。しかし、これは古い言語には当てはまらず、現在の一部の.NET言語にも当てはまらない可能性があります。
マイクローゼンブラム

13

Windowsランタイム(WinRT)はTypedEventHandler<TSender, TResult>デリゲートを導入します。デリゲートStrongTypedEventHandler<TSender, TResult>は、あなたがすることを正確に行いますが、どうやらTResulttypeパラメーターの制約はありません:

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

MSDNドキュメントはこちらです。


1
ああ、進歩があることを確認できて... TResultが 'EventArgs'クラスからの継承に制限されていないのはなぜでしょうか。'EventArgs'基本クラスは基本的に空です。多分彼らはこの制限から離れていますか?
Mike Rosenblum、2012年

設計チームの見落としかもしれません。知るか。
Pierre Arnaud 2012年

まあ、イベントはを使用しなくても正常に機能しますEventArgs。これは単なる慣例です
Sebastian

3
それは、具体的TypedEventHandlerドキュメントで述べargsなりますnull何のイベントデータが存在しない場合、それは、彼らが離れて、デフォルトでは基本的に空のオブジェクトを使用してから取得しているように見えるんので、。元々の考えはEventArgs、型に常に互換性があるため、型の2番目のパラメーターを持つメソッドはどのイベントも処理できるということだったと思います。彼らはおそらく、1つのメソッドで複数の異なるイベントを処理できることがそれほど重要ではないことを理解しているでしょう。
jmcilhinney 2014

1
それは見落としのようには見えません。制約はSystem.EventHandler <TEventArgs>デリゲートからも削除されました。referencesource.microsoft.com/#mscorlib/system/...
colton7909

5

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

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

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

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


2
こんにちはアンドリュー、いいねありがとう!あなたの評判レベルを考えると、これは私にとって本当に意味があります...共分散/反変の問題について:サブスクライバーによって提供されたデリゲートがパブリッシャーのイベントのシグネチャと正確に一致しない場合、共分散と反変が関係します。C#には、デリゲートの共分散と反変が永遠に存在していたため、本質的に感じられますが、VB.NETには.NET 3.0までデリゲートの共分散と反変がありませんでした。したがって、VB.NET for .NET 2.0以前では、このシステムを使用できません。(上記の「編集#2」で追加したコード例を参照してください。)
Mike Rosenblum

@マイク-申し訳ありませんが、あなたは100%正解です!私はあなたのポイントを反映するために私の答えを編集しました:)
Andrew Hare

4
ああ、面白い!デリゲートの共分散/逆分散はCLRの一部であるようですが、(私にはわからない理由で)最新バージョンの前にVB.NETによって公開されていませんでした。言語自体によって有効にされていない場合に、Reflectionを使用してデリゲートの差異を実現する方法を示すFrancesco Balenaによる記事は次のとおりです。dotnet2themax.com / blogs / fbalena /
マイクローゼンブラム

1
@マイク-CLRでサポートされているものの、.NET言語ではサポートされていないものを学ぶのは常に興味深いことです。
Andrew Hare、

5

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

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

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


TEventArgsに一般的な制限を追加する理由 EventHandler <>とTypedEventHandler <、>からは削除されました。実際には意味がなかったためです。
Mike Marynowski

2

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


あなたは正しいかもしれません...一方で、私はそれは単なる「標準」であり、おそらく技術的な問題ではないと思います。つまり、この機能は現在のすべての.NET言語に存在する可能性があります。C#とVB.NETがこれを処理できることは知っています。しかし、これが現在のすべての.NET言語でどの程度広く機能するかはわかりません...しかし、C#とVB.NETで機能し、ここにいる全員が非常に協力的であるため、これを行う可能性は非常に高いと思います。:-)
マイク・ローゼンブラム

2

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

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

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


2

冒とくを振り返って、送信者をオブジェクト型にする唯一の理由として(Microsoftの誤解であるVB 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
はい、System.EventArgsから継承する別のクラスを作成することは直感的に見えず、余分な作業を表す可能性がありますが、それには十分な理由があります。コードを変更する必要がない場合は、アプローチは問題ありません。しかし実際には、将来のバージョンでイベントの機能を増やし、イベント引数にプロパティを追加する必要があるかもしれません。シナリオでは、イベントハンドラーのシグネチャに追加のオーバーロードまたはオプションのパラメーターを追加する必要があります。これはVBAとレガシーVB 6.0で使用されているアプローチであり、実行可能ですが、実際には少し見苦しいです。
Mike Rosenblum

1
ただし、EventArgsから継承することにより、将来のバージョンは古いイベント引数クラスを継承して、それを拡張する可能性があります。古いイベントの呼び出し元はすべて、新しいイベント引数クラスの基本クラスに対して操作することにより、そのまま正確に機能できます。非常にきれいな。あなたのためにもっと働きますが、あなたのライブラリに依存するどんな呼び出し元にとってもきれいです。
Mike Rosenblum

それを継承する必要すらありません。追加の機能をイベント引数クラスに直接追加するだけで、引き続き正常に機能します。つまり、argsをeventargsに固定する制限は、多くのシナリオではあまり意味がないため削除されました。特定のイベントの機能を拡張する必要がないことがわかっている場合、またはパフォーマンスが非常に重要なアプリケーションで必要なのが値型の引数だけである場合。
マイクマリナウスキー

1

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

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

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

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


2
トミー、私が提案しているシステムでこれとまったく同じことができます。「オブジェクト送信者」パラメーターを持つ標準のイベントハンドラーを使用して、これらの厳密に型指定されたイベントを処理できます。(元の投稿に追加したコード例を参照してください。)
Mike Rosenblum

はい、同意します。これは、標準の.NETイベントの良い点です。
Lu4、2011年

1

あなたがしたいことに何か問題があるとは思いません。ほとんどの場合、object sender2.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解決策に傾倒します(個人的に)。ただし、送信者は最初のパラメーターに格納され、イベント引数のプロパティとして格納されるため、冗長であることを知っていればそうします。

に固執することの1つの利点はDataEventArgs、EventArgsが元の送信者を保持しながら、イベントをチェーンして送信者を変更できることです(最後の送信者を表すように)。


ちょっとマイケル、これはかなりきちんとした代替手段です。私はそれが好きです。ただし、ご指摘のとおり、「sender」パラメータを効果的に2回渡すことは冗長です。同様のアプローチがここで議論されています:stackoverflow.com/questions/809609/…、そしてコンセンサスはそれがあまりにも非標準的であるようです。これが、強く型付けされた「送信者」のアイデアをここで提案するのをためらっていた理由です。(ただし、好評のようです。喜んでいます。)
マイク・ローゼンブラム

1

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

public event Action<MyEventType> EventName

MyEventTypeから継承しませんEventArgs。EventArgsのメンバーを使用するつもりがないのに、なぜわざわざ。


1
同意します!なぜサルを感じるべきなのか?
Lu4

1
+1された、これも私が使うものです。時にはシンプルさが勝ちます!またはevent Action<S, T>event Action<R, S, T>など。私はRaiseそれらにスレッドセーフな拡張メソッドがあります:)
nawfal
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.