1

I have an event broker that exposes an EventHandler<T> that allows observers to inspect the event argument and, if needed, modify it. While this works okay, I would ideally like to ensure that T only lives on the stack and, furthermore, that no component is able to take a reference to T, thereby extending its lifetime.

public class Game // mediator pattern
{
  public event EventHandler<Query> Queries; // effectively a chain

  public void PerformQuery(object sender, Query q)
  {
    Queries?.Invoke(sender, q);
  }
}

Sadly, a ref struct cannot be used as a generic argument:

ref struct Query {} // EventHandler<Query> not allowed

And similarly I cannot imbue EventHandler's TEventArgs with any sort of 'use structs, pass by reference' mechanics.

Now, in C#, we can decide whether variables live on the stack on the heap, e.g. with stackalloc and such, so what I'm after, I guess, is just a way of getting something equivalent to a ref struct inside an event.

Dmitri Nesteruk
  • 23,067
  • 22
  • 97
  • 166

1 Answers1

2

While stackalloc applied/wrapped in a very convoluted way may (probably) give you some semblance of variables live on the stack on the heap, it will not be what stackalloc is intended for.

So I'd rather propose to concentrate on the no component is able to take a reference to T, thereby extending its lifetime part.

To get it we need

  1. Wrapper class (probably, but not necessary with corresponding interface)
  2. Implementing IDisposable
  3. And storing the actual T as WeakReference

It will be something like

public interface ITakeNoRefClass
{
    void Change(string value);
}

public class TakeNoRefClass : ITakeNoRefClass
{
    ...
}

public class TakeNoRefClassWrapper : ITakeNoRefClass, IDisposable
{
    private bool _isDisposed;
    private readonly WeakReference<TakeNoRefClass> _takeNoRefWeakRef;

    public TakeNoRefClassWrapper(WeakReference<TakeNoRefClass> takeNoRefWeakRef)
    {
        _takeNoRefWeakRef = takeNoRefWeakRef;
    }

    public void Change(string value)
    {
        Execute(o => o.Change(value));
    }

    private void Execute(Action<ITakeNoRefClass> action)
    {
        if (_disposed)
        {
            throw new ObjectDisposedException("You should not have taken this ref");
        }
        var target = _takeNoRefWeakRef.Target;
        if (target == null)
        {
            throw new ObjectDisposedException("You should not have taken this ref");
        }
        action(target);
    }

    public void Dispose()
    {
        _isDisposed = true;
    }
}

And it should be used like

public void CreateObjectAndRaiseEvents()
{
    var target = new TakeNoRefClass();
    // Passing it into a separate method to ensure that it won't be GC'ed before executing all event handlers.
    RaiseEvents(target);
}

private void RaiseEvent(TakeNoRefClass target)
{
    using (var wrapper = new TakeNoRefClassWrapper(new WeakReference<TakeNoRefClass>(target))
    {
        _event?.Invoke(wrapper);
    }
}
Eugene Podskal
  • 10,270
  • 5
  • 31
  • 53