F # Observable.add does not provide a way to cancel the event, so I think it is less likely to be a source of leaks
In fact, the opposite is true. Observable.add
, according to documents, constantly subscribes to an event and causes a "leak". It effectively performs the addition of an event handler that cannot unsubscribe.
In general, with Observable
(in F # and C #), you should use .subscribe
and manage your subscription descriptor when you're done.
As mentioned in @rmunn, Gjallarhorn can serve as an alternative to using observables in some scenarios (and if necessary combined with them). When writing it, one of my main goals was to ensure that subscriptions do not leak out - all subscriptions use a hybrid push / pull model, based on weak links, which prevents many problems with leakage in events and based on observable code.
To demonstrate, I chose a code variation using both observable and Gjallarhorn signals. If you run this in the release build, outside the debugger you will 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 subscribe to it in a separate area and throw out a one-time subscription subscriber ( Observable.add
- this is nothing but subscribe |> ignore
- see code for details. ) . When you run the release build outside the debugger (the debugger prevents cleaning), 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 observed case, the .add
call constantly contains a link to the notifier, preventing it from collecting garbage. With signals, the subscription to the signal will be GC and will "unhook" automatically, preventing the display of calls from Hit.