Search code examples
c#.net-coresystem.reactive

EventHandler vs Rx Subject


I'm using System.Reactive and I don't know which one to choose: EventHandlers or Subjects. What's the difference between them?

var client = WebSocketClient.Create(uri);

// Subject
client.OnConnected
    .Subscribe(_ => { Log.Information($"Socket {client.Id} connected"); })
    .DisposeWith(disposable);

// EventHandler
Observable
    .FromEventPattern(h => client.Connected += h, h => client.Connected -= h)
    .Select(_ => Unit.Default)
    .Subscribe(_ => { Log.Information($"Socket {client.Id} connected"); })
    .DisposeWith(disposable);
public class WebSocketClient : IWebSocketClient
{
    // Subject
    private readonly ISubject<Unit> _connectedSubject = new Subject<Unit>();

    public IObservable<Unit> OnConnected => _connectedSubject.AsObservable();

    // EventHandler
    private EventHandler? _connected;

    public event EventHandler Connected
    {
        add => _connected += value;
        remove => _connected -= value;
    }

    // Logic
    async Task IWebSocketClient.ConnectAsync(CancellationToken cancellationToken)
    {
        ...

        await _webSocket.ConnectAsync(_uri, cancellationToken).ConfigureAwait(false);

        _connected?.Invoke(this, EventArgs.Empty);

        _connectedSubject.OnNext();

        ...
    }

    private void Dispose()
    {
        _connectedSubject.OnCompleted();
    }
}

Solution

  • Here's an example bit of code that better illustrates the use of a subject versus an event to create an observable.

    public class Foo
    {
        private event EventHandler<Unit> _bang;
        
        public IObservable<Unit> Bangs =>
            Observable
                .FromEventPattern<Unit>(h => _bang += h, h => _bang -= h)
                .Select(x => x.EventArgs);
        
        private Subject<Unit> _boom = new Subject<Unit>();
    
        public IObservable<Unit> Booms =>
            _boom.AsObservable();
                
        public void OnExplode()
        {
            _bang?.Invoke(this, Unit.Default);
            _boom.OnNext(Unit.Default);
        }
    }
    

    Now I can execute it:

        var foo = new Foo();
    
        foo.Bangs.Subscribe(_ => Console.WriteLine("Bang!"));
        foo.Booms.Subscribe(_ => Console.WriteLine("Boom!"));
    
        foo.OnExplode();
    

    The result I get on the console is this:

    Bang!
    Boom!
    

    The code does the job well.

    Now, the concern with both approaches is a nefarious coder of the Foo class. They could add this method:

    public void Nefarious()
    {
        _boom.OnCompleted();
        _bang = null;
    }
    

    That effectively kills the code.

    Now I could run this:

    var foo = new Foo();
    
    foo.Bangs.Subscribe(_ => Console.WriteLine("Bang!"));
    foo.Booms.Subscribe(_ => Console.WriteLine("Boom!"));
    
    foo.OnExplode();
    foo.Nefarious();
    foo.OnExplode();
    

    And I'll still only see the output once.

    Both can be corrupted.

    In general, though, I find that few people set their event delegates to null.

    It's far more likely that a subject will be ended or error, thus stopping the code form working.