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
OnParentSet isn't guaranteed to work and carries other problems.)
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
WeakSubscriber type below.
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.
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
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