イベントプロシージャが呼ばれる順序について

タグの編集
投稿者 あにす  (社会人) 投稿日時 2009/2/4 23:10:13
日本語だと上手く説明出来ないのでまずはコードを見て下さい。
VB.NETでイベントを発生させるだけの意味の無いコードです。
Module Module1

    Sub Main()
        Dim hoge As New Hoge

        AddHandler hoge.Event_, New EventHandler(AddressOf A)
        AddHandler hoge.Event_, New EventHandler(AddressOf B)
        AddHandler hoge.Event_, New EventHandler(AddressOf C)

        hoge.RaiseEvent_()

        Console.ReadLine()
    End Sub

    Sub A()
        Console.WriteLine("A")
    End Sub

    Sub B()
        Console.WriteLine("B")
    End Sub

    Sub C()
        Console.WriteLine("C")
    End Sub
End Module

Delegate Sub EventHandler()

Class Hoge
    Public Event Event_ As EventHandler

    Public Sub RaiseEvent_()
        RaiseEvent Event_()
    End Sub
End Class


僕の環境では何度実行しても結果は

A
B
C

という出力になるのですが、これはVBの仕様で保障されているのでしょうか?それとも処理系の実装依存でたまたまなのでしょうか?
特にこのことで困っているわけでもないのですが、どうにも気になってしまってスッキリしないのです。
知っている方がいましたら是非情報を下さい。

P.S. VB2005でビルドしたバイナリをVMware上に構築したubuntuでmono上で実行しても結果は変わりませんでした。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2009/2/5 01:08:20
> 処理系の実装依存でたまたまなのでしょうか
特に決まりは無かったと思います。ゆえに回答としては、
「処理順は保証されないが、通常は登録順に処理される物と予想される」では無いかと。


> 困っているわけでもないのですが、どうにも気になってしまって
もし、AddHandeler の登録順に依存した実装にしたい場合には、カスタムイベントを
利用できるでしょう。たとえば、登録の逆順(つまり、C,B,A の順)に処理させたいのであれば:
Class Hoge
    'Public Event Event_ As EventHandler 

    Public Custom Event Event_ As EventHandler
        AddHandler(ByVal value As EventHandler)
            EventList.Insert(0, value)
        End AddHandler

        RemoveHandler(ByVal value As EventHandler)
            EventList.Remove(value)
        End RemoveHandler

        RaiseEvent()
            For Each method As EventHandler In EventList
                method()
            Next
        End RaiseEvent
    End Event
    Private EventList As New List(Of EventHandler)()

    Public Sub RaiseEvent_()
        RaiseEvent Event_()
    End Sub
投稿者 あにす  (社会人) 投稿日時 2009/2/5 02:26:37
やっぱり順番を宛にしちゃダメだったんですね。とてもスッキリしました、ありがとうございます。
カスタムイベントというは初めて見ました。描画系とかで順番が重要な場面で使えますね。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2009/2/5 16:05:34
> やっぱり順番を宛にしちゃダメだったんですね。
ですね。そもそも WithEvents されたら、順番も何もあったものでは無いですし。

> カスタムイベントというは初めて見ました。
EventHandlerList クラスについても調べてみると良いかも。
http://msdn.microsoft.com/ja-jp/library/yt1k2w4e.aspx

カスタムイベントの使いどころとしては、このあたりでしょうか。
 ・イベント登録数をカウントしておき、0件ならばイベント関連の処理判定を行わないようにして、処理の高速化。
 ・特定のインスタンスのみ、イベントを通知しないようなフィルタリング作業。
 ・イベント通知の際にログを出力させるような実装。
 ・非同期イベントの実装。(処理時間の長いイベントハンドラが、後続のイベントをブロックしないようにする)

> 描画系とかで順番が重要な場面で使えますね。 
描画をクラス側で行う場合でしょうか。それとも、受け側で行う場合でしょうか。
描画順番が意味を持つ状況は容易に想像できますが、それがイベント処理と
どう繋がってくるのか、擬似コードをイメージできませんでした。
投稿者 あにす  (社会人) 投稿日時 2009/2/5 17:36:52
こんな感じでしょうか。これなら登録順に依存して描画させられるなぁと…。

Imports System.Windows.Forms
Imports System.Drawing

Module Module1
    Public Sub main()
        Application.Run(New MainForm())
    End Sub
End Module

Class MainForm
    Inherits Form

    Dim pb As CustomPictureBox

    Public Sub New()
        pb = New CustomPictureBox()
        pb.Dock = DockStyle.Fill
        Me.Controls.Add(pb)

        AddHandler pb.Paint, New PaintEventHandler(AddressOf DrawBackGround)
        AddHandler pb.Paint, New PaintEventHandler(AddressOf drawBlackRect)
        AddHandler pb.Paint, New PaintEventHandler(AddressOf drawRedRect)
    End Sub

    Sub DrawBackGround(ByVal sender As ObjectByVal e As PaintEventArgs)
        e.Graphics.FillRectangle(Brushes.White, e.ClipRectangle)
    End Sub

    Sub drawBlackRect(ByVal sender As ObjectByVal e As PaintEventArgs)
        e.Graphics.FillRectangle(Brushes.Black, New RectangleF(10, 10, 100, 100))
    End Sub

    Sub drawRedRect(ByVal sender As ObjectByVal e As PaintEventArgs)
        e.Graphics.FillRectangle(Brushes.Red, New RectangleF(20, 20, 100, 100))
    End Sub

End Class

Class CustomPictureBox
    Inherits PictureBox

    Public Shadows Custom Event Paint As PaintEventHandler
        AddHandler(ByVal value As PaintEventHandler)
            EventList.Add(value)
        End AddHandler

        RemoveHandler(ByVal value As PaintEventHandler)
            EventList.RemoveAt(EventList.LastIndexOf(value))
        End RemoveHandler

        RaiseEvent(ByVal sender As ObjectByVal e As System.Windows.Forms.PaintEventArgs)
            For Each method As PaintEventHandler In EventList
                method(sender, e)
            Next
        End RaiseEvent
    End Event

    Private EventList As New List(Of PaintEventHandler)

    Sub basePaint(ByVal sender As ObjectByVal e As PaintEventArgs) Handles MyBase.Paint
        RaiseEvent Paint(sender, e)
    End Sub
End Class
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2009/2/5 18:26:55
その場合でも、わざわざカスタムイベント化するほどのメリットは無いような気がします。
利用側の実装としては下記で充分だと思いますし。

Private WithEvents pb As CustomPictureBox

Private Sub pb_Paint(ByVal sender As ObjectByVal e As PaintEventArgs) Handles pb.Paint
  DrawBackGround(e.Graphics, e.ClipRectangle)
  DrawBlackRect(e.Graphics, New Rectangle(10, 10, 100, 100))
  DrawRedRect(e.Graphics, New Rectangle(20, 20, 100, 100))
End Sub

Private Sub DrawBackGround(ByVal g As Graphics, ByVal r As RectAngle)
  g.FillRectangle(Brushes.White, r)
End Sub

Private Sub DrawBlackRect(ByVal g As Graphics, ByVal r As RectAngle)
  g.FillRectangle(Brushes.Black, r)
End Sub

Private Sub DrawRedRect(ByVal g As Graphics, ByVal r As RectAngle)
  g.FillRectangle(Brushes.Red, r)
End Sub
投稿者 あにす  (社会人) 投稿日時 2009/2/5 19:10:30
>利用側の実装としては下記で充分だと思いますし。
はい。僕も今まではそうしていたのですが、実行中に描画の有無を動的に切り替えたい場合だとフラグを用意して、描画用のメソッドの中でフラグを判定して…という感じでなかなか面倒なことになっていたので先の投稿のコードを思い付きました。
でも、利用側でInsert出来ないと、RemoveHandlerしたら元に戻せなくなっちゃいますね…。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2009/2/5 19:50:30
> 実行中に描画の有無を動的に切り替えたい場合だとフラグを用意して

そういう切り替えは、「描画担当」のクラスが行うべきなので、
この場合の描画順は、やはりフォーム側で制御するべきでしょう。


もし、クラス側に描画を担当させるような実装にするのであれば、クラス側の実装は
イベントとしてではなく、デリゲートを登録させる実装の方が良いかも知れません。
投稿者 あにす  (社会人) 投稿日時 2009/2/5 20:34:13
>もし、クラス側に描画を担当させるような実装にするのであれば、クラス側の実装は
>イベントとしてではなく、デリゲートを登録させる実装の方が良いかも知れません。 

なんか話が難しくなってきたのですが(笑)こういうことでしょうか?
Imports System.Windows.Forms
Imports System.Drawing

Module Module1
    Public Sub main()
        Application.Run(New MainForm())
    End Sub
End Module

Class MainForm
    Inherits Form

    Dim pb As CustomPictureBox

    Public Sub New()
        pb = New CustomPictureBox()
        pb.Dock = DockStyle.Fill
        Me.Controls.Add(pb)

        pb.RectList.Add(New PaintEventHandler(AddressOf DrawBackGround))
        pb.RectList.Add(New PaintEventHandler(AddressOf drawBlackRect))
        pb.RectList.Insert(1, New PaintEventHandler(AddressOf drawRedRect))
    End Sub

    Sub DrawBackGround(ByVal sender As ObjectByVal e As PaintEventArgs)
        e.Graphics.FillRectangle(Brushes.White, e.ClipRectangle)
    End Sub

    Sub drawBlackRect(ByVal sender As ObjectByVal e As PaintEventArgs)
        e.Graphics.FillRectangle(Brushes.Black, New RectangleF(10, 10, 100, 100))
    End Sub

    Sub drawRedRect(ByVal sender As ObjectByVal e As PaintEventArgs)
        e.Graphics.FillRectangle(Brushes.Red, New RectangleF(20, 20, 100, 100))
    End Sub

End Class

Class CustomPictureBox
    Inherits PictureBox

    Public RectList As New List(Of PaintEventHandler)

    Sub CustomPictureBox_Paint(ByVal sender As ObjectByVal e As PaintEventArgs) Handles Me.Paint
        For Each method As PaintEventHandler In rectList
            method(sender, e)
        Next
    End Sub
End Class

[delegate].Combine()使うとなんか不便そうなのでList(Of PaintEventHandler)で持たせました。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2009/2/6 10:13:23
> なんか話が難しくなってきたのですが
まぁ、最初の質問からして中級者以上の話に見えましたし。

で。

そもそも、先の要件において、処理順番を意識したコードにする事が良い設計と言えるかと問われれば、個人的には 否 と答えます。

描画順が重要な意味を持つのであれば、それらを分離して、たとえば「背景描画イベント」「前景描画イベント」などに分離した方が分かり易いのでは無いでしょうか(たとえば DataGridView における RowPrePaint, RowPostPaint, CellPainting イベントのように)。もし、イベントの数を無闇に増やしたくないのであれば、DataGridViewCellPaintingEventArgs における PaintParts プロパティ(と Paint メソッド)のように、そのイベントにて何を描画させたいのかを示すような実装にした方が、「順番」で管理するよりも、処理の意図が分かりやすくなるかと思います。


> こういうことでしょうか?
肝心の『実行中に描画の有無を動的に切り替えるためのフラグ』のコードが抜けているので判断に困りますが、実装案の一つになるとは思います。OnPaint メソッドではなく Paint イベントを利用してしまっている点と、List を Public フィールドにしている点は改善の余地がありそうですが。
投稿者 あにす  (社会人) 投稿日時 2009/2/6 13:06:26
>処理順番を意識したコードにする事が良い設計と言えるかと問われれば、個人的には 否
そうですね。後から順番の修正が必要になったときにソースを追うのも大変そうです。

>OnPaint メソッドではなく Paint イベントを利用してしまっている点と、List を Public フィールドにしている点は改善の余地がありそうですが。
OnPaintメソッドについては、前回のソースを修正しながら書いてたらうっかりしてました。実は何故OnPaintメソッドをオーバーライドする方がいいのかは良くわからないのですが…。利用側からRemoveHandlerされる危険がないからでしょうか?
Listについては、これはIList型のプロパティにして公開した方が他のListにごそっと入れ替えられなくて安全ということでしょうか。