Home > Software design >  Why I cant make an ref this Action extension method?
Why I cant make an ref this Action extension method?

Time:10-05

I wanted to create a snippet like following:

public static class Program
{
    public static event Action OnEvent;

    public static void Main()
    {
        var registration = OnEvent.Subscribe(() => { Console.Write("OnEventInvoked"); });
        OnEvent?.Invoke();
        registration.Dispose();
    }

    public static CancellationTokenRegistration Subscribe(ref this Action action, Action callback)
    {
        action  = callback;
        return new CancellationToken().Register(() => { action -= callback; });
    }
}

But I cant, because Action is not an structure :/

The reason I want this is:
1 - By having the event modifier on the Action, I close it for invokations outside of the declaring class.
2 - By having this Subscribe extension method, Im capable of being able to unsubscribe from it without having to store the action and then using -= later on.

So, because of number 1, I cannot create a custom class containing an action, since it would make Invoke public available.
Has someone already tackled something like it?
Thanks in advance!

CodePudding user response:

I assume that you are familiar with properties, and how the compiler can auto create the backing field and access methods;

public static string Foo { get; set; }

// becomes
private static string _Foo;
public static string Foo {
    get => _Foo;
    set => _Foo = value;
}

And that since a property merely defines two methods on the interface of a type, you can't take a reference to it.

Events are auto implemented in a similar way.

public static event Action OnEvent;
OnEvent();

// becomes;
private static Action m_event;
public static event Action OnEvent
{
    add
    {
        // combine the delegate, keep trying if we have a race between threads
        var action = m_event;
        while (true)
        {
            var action2 = action;
            var value2 = (Action)Delegate.Combine(action2, value);
            action = Interlocked.CompareExchange(ref m_event, value2, action2);
            if ((object)action == action2)
                break;
        }
    }
    remove
    {
        // remove the delegate, keep trying if we have a race between threads
        var action = m_event;
        while (true)
        {
            var action2 = action;
            var value2 = (Action)Delegate.Remove(action2, value);
            action = Interlocked.CompareExchange(ref m_event, value2, action2);
            if ((object)action == action2)
                break;
        }
    }
}
m_event();

And for the same reasons, the add and remove methods become part of the interface. With no way to take a reference to them.

CodePudding user response:

There's one way that I've seen events being able to be passed by reference.

First up you need to define a reference event:

public interface IAnonymousEvent<TDelegate>
{
    IDisposable Subscribe(TDelegate handler);
}

It's "Anonymous" as this decouples the event from the source.

Next I create a static Event class and create the implementation of the IAnonymousEvent<TDelegate> in there:

public static class Event
{
    private class AnonymousEvent<TDelegate> : IAnonymousEvent<TDelegate>
    {
        private readonly Action<TDelegate> _addHandler;
        private readonly Action<TDelegate> _removeHandler;

        public AnonymousEvent(Action<TDelegate> addHandler, Action<TDelegate> removeHandler)
        {
            _addHandler = addHandler;
            _removeHandler = removeHandler;
        }

        public IDisposable Subscribe(TDelegate handler)
        {
            _addHandler(handler);
            return new AnonymousDisposable(() => _removeHandler(handler));
        }
    }
}

This requires an AnonymousDisposable class to do the clean up:

public sealed class AnonymousDisposable : IDisposable
{
    private readonly Action _action;
    private int _disposed;

    public AnonymousDisposable(Action action)
    {
        _action = action;
    }

    public void Dispose()
    {
        if (Interlocked.Exchange(ref _disposed, 1) == 0)
        {
            _action();
        }
    }
}

And finally we can write an MakeAnonymous method in Event to create the IAnonymousEvent<TDelegate> instances:

public static IAnonymousEvent<TDelegate> MakeAnonymous<TDelegate>(Action<TDelegate> add, Action<TDelegate> remove) =>
    new AnonymousEvent<TDelegate>(add, remove);

So, given all of this I can now write this:

public static event Action SomeEvent;

public static void Main()
{
    IAnonymousEvent<Action> se = Event.MakeAnonymous<Action>(h => SomeEvent  = h, h => SomeEvent -= h);
    SomeEvent?.Invoke();
    IDisposable subscription = se.Subscribe(() => Console.WriteLine("SomeEvent!"));
    SomeEvent?.Invoke();
    subscription.Dispose();
    SomeEvent?.Invoke();
}

When I run that code I get a single line of SomeEvent! showing that I can subscribe and unsubscribe to the original event purely through the IAnonymousEvent<Action> instance. And I can pass that instance around as reference to any code.

This code can be extended with any delegate type:

public static event Action<string> SomeOtherEvent;
public static event EventHandler<string> SomeOtherEvent2;

public static void Main()
{
    IAnonymousEvent<Action<string>> soe1 = Event.MakeAnonymous<Action<string>>(h => SomeOtherEvent  = h, h => SomeOtherEvent -= h);
    SomeOtherEvent?.Invoke("B");
    IDisposable subscription11 = soe1.Subscribe(x => Console.WriteLine($"Hello {x}"));
    SomeOtherEvent?.Invoke("B");
    IDisposable subscription12 = soe1.Subscribe(x => Console.WriteLine($"Goodbye {x}"));
    SomeOtherEvent?.Invoke("C");
    subscription11.Dispose();
    SomeOtherEvent?.Invoke("D");
    subscription12.Dispose();
    SomeOtherEvent?.Invoke("E");

    IAnonymousEvent<EventHandler<string>> soe2 = Event.MakeAnonymous<EventHandler<string>>(h => SomeOtherEvent2  = h, h => SomeOtherEvent2 -= h);
    SomeOtherEvent2?.Invoke(new object(), "B");
    IDisposable subscription21 = soe2.Subscribe((s, e) => Console.WriteLine($"!Hello {e}"));
    SomeOtherEvent2?.Invoke(new object(), "B");
    IDisposable subscription22 = soe2.Subscribe((s, e) => Console.WriteLine($"!Goodbye {e}"));
    SomeOtherEvent2?.Invoke(new object(), "C");
    subscription21.Dispose();
    SomeOtherEvent2?.Invoke(new object(), "D");
    subscription22.Dispose();
    SomeOtherEvent2?.Invoke(new object(), "E");
}

That produces:

Hello B
Hello C
Goodbye C
Goodbye D
!Hello B
!Hello C
!Goodbye C
!Goodbye D
  • Related