3
\$\begingroup\$

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
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.