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