In this SO question I needed an observable subscription function that can be used kind of like Observable.subscribe
, but that does not keep a strong reference to the listener.
I have arrived at a relatively simple function that seems to work correctly, though I'm relatively fresh at F# and know very little about garbage collection, so I would please ask anyone more knowledgeable in such matters to review my solution for potential bugs or simpler solutions (I also highly welcome feedback on its style and idiomaticness).
The solution is loosely based on information from Samuel Jack's Weak Events in .Net, the easy way as well as solution 4 in CodeProject's Weak Events in C#.
(Short background: My main use for this will be subscribing Xamarin.Forms pages to a long-lived state container that publishes state updates. There is no reliably way to determine when a page is permanently removed, and the pages should be able to be garbage collected. Subscribing/unsubscribing in OnAppearing
/OnDisappearing
or OnParentSet
isn't guaranteed to work and carries other problems.)
Implementation
module Observable =
open System
// ('a -> 'b -> unit) -> 'a -> IObservable<'b> -> IDisposable
let subscribeWeakly callback target source =
let mutable sub:IDisposable = null
let mutable disposed = false
let wr = new WeakReference<_>(target)
let dispose() =
lock (sub) (fun () ->
if not disposed then sub.Dispose(); disposed <- true)
let callback' x =
let isAlive, target = wr.TryGetTarget()
if isAlive then callback target x else dispose()
sub <- Observable.subscribe callback' source
sub
Usage example
See the WeakSubscriber
type below.
Caveat
You have to use the callback's me
parameter to invoke any relevant members. If you use this
inside the callback, you'll still end up with a strong reference for reasons described in the aforementioned articles. For the same reason (I guess? Please clarify), you can't invoke a "plain" function in the class defined using let
. If there is any way around this (i.e., to make it more foolproof without making it harder to use), I'm highly interested.
Testing
Helper classes:
type Publisher() =
let myEvent = new Event<_>()
[<CLIEvent>] member this.MyEvent = myEvent.Publish
member this.Trigger(x) = myEvent.Trigger(x)
type StrongSubscriber() =
member this.MyMethod x =
printfn "Strong: method received %A" x
member this.Subscribe(publisher:Publisher) =
publisher.MyEvent |> Observable.subscribe this.MyMethod
publisher.MyEvent |> Observable.subscribe
(fun x -> printfn "Strong: lambda received %A" x)
type WeakSubscriber() =
member this.MyMethod x =
printfn "Weak: method received %A" x
member this.Subscribe(publisher:Publisher) =
publisher.MyEvent |> Observable.subscribeWeakly
(fun (me:WeakSubscriber) x -> me.MyMethod x) this
publisher.MyEvent |> Observable.subscribeWeakly
(fun _ x -> printfn "Weak: lambda received %A" x) this
The actual test:
[<EntryPoint>]
let main argv =
let pub = Publisher()
let doGc() =
System.GC.Collect()
System.GC.WaitForPendingFinalizers()
System.GC.Collect()
printfn "\nGC completed\n"
let someScope() =
let strong = StrongSubscriber()
let weak = WeakSubscriber()
strong.Subscribe(pub)
weak.Subscribe(pub)
doGc() // should not remove weak subscription since it's still in scope
printfn "All subscribers should still be triggered:"
pub.Trigger(1)
someScope()
doGc() // should remove weak subscriptions
printfn "Weak subscribers should not be triggered:"
pub.Trigger(2)
System.Console.ReadKey() |> ignore
0
Output:
GC completed
All subscribers should still be triggered:
Strong: method received 1
Strong: lambda received 1
Weak: method received 1
Weak: lambda received 1
GC completed
Weak subscribers should not be triggered:
Strong: method received 2
Strong: lambda received 2