Since observables are typically IDisposable
how does that change, if at all, the need to use weak references in event handlers, or any other event based memory leak/GC locked referencing?
While my primary concern/need is for WPF I'm looking for the broader example and trying to understand where I may need weak references.
F#'s Observable.add
doesn't provide a way to unhook the event, so I'm thinking it's less likely to be a source of leaks.
Sample code:
type Notifier() =
let propChanged = new Event<_,_>()
member __.Foo() = ()
interface INotifyPropertyChanged with
[<CLIEvent>]
member __.PropertyChanged = propChanged.Publish
abstract member RaisePropertyChanged : string -> unit
default x.RaisePropertyChanged(propertyName : string) = propChanged.Trigger(x, PropertyChangedEventArgs(propertyName))
Notifier() :?> INotifyPropertyChanged
|> Observable.add(fun _ -> printfn "I'm hooked on you")
F#'s Observable.add doesn't provide a way to unhook the event, so I'm thinking it's less likely to be a source of leaks
It's actually the opposite. Observable.add
, by the docs, permanently subscribes to the event, and forces a "leak". It's effectively doing an event handler addition that has no way to unsubscribe.
In general, with Observable
(in F# and C#), you should favor using .subscribe
, and disposing of the subscription handle when you're done.
As @rmunn mentioned, Gjallarhorn can serve as an alternative to using observables in some scenarios (and integrates nicely with them as needed). While writing it, one of my main goals was to make it so that subscriptions don't leak - all of the subscriptions use a hybrid push/pull model based on weak references, which prevents many of the problems with leaking in event and observable based code.
To demonstrate, I've thrown together a variation on your code, using both observables and Gjallarhorn's signals. If you run this in a release build, outside of the debugger, you'll see the difference:
type Notifier() =
let propChanged = new Event<_,_>()
member __.Foo() = ()
interface INotifyPropertyChanged with
[<CLIEvent>]
member __.PropertyChanged = propChanged.Publish
abstract member RaisePropertyChanged : string -> unit
default x.RaisePropertyChanged(propertyName : string) = propChanged.Trigger(x, PropertyChangedEventArgs(propertyName))
let obs () =
use mre = new ManualResetEvent(false)
let not = Notifier()
do
let inpc = not :> INotifyPropertyChanged
inpc.PropertyChanged
|> Observable.add (fun p -> printfn "Hit %s!" p.PropertyName)
async {
for i in [0 .. 10] do
do! Async.Sleep 100
printfn "Raising"
not.RaisePropertyChanged <| sprintf "%d" i
mre.Set () |> ignore
} |> Async.Start
printfn "Exiting block"
GC.Collect() // Force a collection, to "cleanup"
mre.WaitOne() |> ignore
let signals () =
use mre = new ManualResetEvent(false)
let not = Mutable.create 0
do
not
|> Signal.Subscription.create (fun v -> printfn "Hit %d!" v)
|> ignore // throw away subscription handle
async {
for i in [0 .. 10] do
do! Async.Sleep 100
printfn "Setting"
not.Value <- i
mre.Set () |> ignore
} |> Async.Start
printfn "Exiting block"
GC.Collect() // Force a collection, to "cleanup"
mre.WaitOne() |> ignore
[<STAThread>]
[<EntryPoint>]
let main _ =
printfn "Using observable"
obs ()
printfn "Using signals"
signals ()
1
Note that both do something similar - they create a "source", then, in a separate scope, subscribe to it and throw away the disposable subscription handle (Observable.add
is nothing but subscribe |> ignore
- see code for details.). When running in a release build outside of the debugger (the debugger prevents cleanup from happening), you see:
Using observable
Exiting block
Raising
Hit 0!
Raising
Hit 1!
Raising
Hit 2!
Raising
Hit 3!
Raising
Hit 4!
Raising
Hit 5!
Raising
Hit 6!
Raising
Hit 7!
Raising
Hit 8!
Raising
Hit 9!
Raising
Hit 10!
Using signals
Exiting block
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Press any key to continue . . .
In the observable case, the call to .add
permanently holds a reference to the notifier, preventing it from being garbage collected. With signals, the signal subscription will GC, and "unhook" automatically, preventing the calls from Hit from ever being displayed.