読者です 読者をやめる 読者になる 読者になる

かたちづくり

つれづれに、だらだらと、おきらくに

Weak Event パターン

Weak Event パターンをもっと簡単に実装する方法はないのかな、と思って色々と試行錯誤してみました。あんまり自信はありませんが、とりあえずこれでいいのかな、というものが出来たので晒してみます。変なとこあったら教えて下さい m(_ _)m

まず Weak Event でない場合にどんな問題が起こるか、簡単なプログラムで再現させてみます。

open System

type EventSource () =
  let event = Event<EventHandler, EventArgs> ()
  [<CLIEvent>] member __.Event = event.Publish
  member __.Fire () = event.Trigger (null, EventArgs.Empty)

type EventListener (source : EventSource, id) =
  let subscribe = source.Event |> Observable.subscribe (fun _ -> printf "%d, " id)
  override __.Finalize () = printfn "finalized %d" id; subscribe.Dispose ()

let source = EventSource ()
let mutable listener = Unchecked.defaultof<EventListener>
for id = 1 to 10 do
  listener <- EventListener (source, id)
  source.Fire ()
  GC.Collect()
  printfn ""

この出力結果は次のようになります。

1,
1, 2,
1, 2, 3,
1, 2, 3, 4,
1, 2, 3, 4, 5,
1, 2, 3, 4, 5, 6,
1, 2, 3, 4, 5, 6, 7,
1, 2, 3, 4, 5, 6, 7, 8,
1, 2, 3, 4, 5, 6, 7, 8, 9,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
finalized 10
finalized 1
finalized 9
finalized 8
finalized 7
finalized 6
finalized 5
finalized 4
finalized 3
finalized 2
続行するには何かキーを押してください . . .

listener 変数を書き換えているにも関わらず、イベントハンドラがイベントソースに強参照で登録されているために listener が全く Finalize されていないことが見て取れます。

そこで EventListener を次のように書き換えます。

type EventListener (source : EventSource, id) =
  let callback = Action<EventArgs> (fun _ -> printf "%d, " id)  // コールバックは強参照で保持
  let subscribe =
    let callbackRef = WeakReference<_> callback // イベントに登録するのは弱参照
    source.Event |> Observable.subscribe (fun arg ->
      match callbackRef.TryGetTarget () with true, callback -> callback.Invoke arg | _ -> ())
  override __.Finalize () = printfn "finalized %d" id; subscribe.Dispose ()
  member private __.__ = callback // これがないと callback フィールドが作られない!

これで再び実行すると次のような結果になりました。

1,
1, 2,
2, 3,
3, 4, finalized 1

4, 5, finalized
5, 2
finalized 3
finalized 4
6,
finalized 5
6, 7,
7, 8, finalized 6
finalized 7

8, 9,
9, 10, finalized 8
finalized
9
finalized 10
続行するには何かキーを押してください . . .

ループの途中で EventListener が Finalize されていることが分かります。