Подписка на одноразовые события

Я уверен, что это невозможно, но я все равно спрошу.

Чтобы сделать однократную подписку на события, я часто нахожусь в использовании этого (самостоятельно изобретенного) шаблона:

EventHandler handler=null;
handler = (sender, e) =>
{
    SomeEvent -= handler;
    Initialize();
};
SomeEvent += handler;

Это довольно много котельной плиты, и это также заставляет Resharper зависеть от модифицированных закрытий. Есть ли способ превратить этот шаблон в метод расширения или аналогичный? Лучший способ сделать это?

В идеале мне бы хотелось что-то вроде:

SomeEvent.OneShot(handler)

Ответ 1

Нелегко реорганизовать метод расширения, потому что единственный способ, которым вы можете ссылаться на событие на С#, - подписаться (+=) на или отменить подписку (-=) от него (если он не был объявлен в текущий класс).

Вы можете использовать тот же подход, что и в Reactive Extensions: Observable.FromEvent принимает двух делегатов, чтобы подписаться на событие, чтобы отказаться от него. Поэтому вы можете сделать что-то вроде этого:

public static class EventHelper
{
    public static void SubscribeOneShot(
        Action<EventHandler> subscribe,
        Action<EventHandler> unsubscribe,
        EventHandler handler)
    {
        EventHandler actualHandler = null;
        actualHandler = (sender, e) =>
        {
            unsubscribe(actualHandler);
            handler(sender, e);
        };
        subscribe(actualHandler);
    }
}

...

Foo f = new Foo();
EventHelper.SubscribeOneShot(
    handler => f.Bar += handler,
    handler => f.Bar -= handler,
    (sender, e) => { /* whatever */ });

Ответ 2

Следующий код работает для меня. Это не идеально, чтобы указать событие через строку, но у меня нет клея, как это решить. Я думаю, это невозможно в текущей версии С#.

using System;
using System.Reflection;

namespace TestProject
{
    public delegate void MyEventHandler(object sender, EventArgs e);

    public class MyClass
    {
        public event MyEventHandler MyEvent;

        public void TriggerMyEvent()
        {
            if (MyEvent != null)
            {
                MyEvent(null, null);
            }
            else
            {
                Console.WriteLine("No event handler registered.");
            }
        }
    }

    public static class MyExt
    {
        public static void OneShot<TA>(this TA instance, string eventName, MyEventHandler handler)
        {
            EventInfo i = typeof (TA).GetEvent(eventName);
            MyEventHandler newHandler = null;
            newHandler = (sender, e) =>
                             {
                                 handler(sender, e);
                                 i.RemoveEventHandler(instance, newHandler);
                             };
            i.AddEventHandler(instance, newHandler);
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            MyClass c = new MyClass();
            c.OneShot("MyEvent",(sender,e) => Console.WriteLine("Handler executed."));
            c.TriggerMyEvent();
            c.TriggerMyEvent();
        }
    }
}

Ответ 3

Я бы предложил использовать "настраиваемое" событие, чтобы у вас был доступ к списку вызовов, а затем поднять событие, используя Interlocked.Exchange, чтобы одновременно читать и очищать список вызовов. При желании, подписка на события/отказ от подписки/повышение может быть выполнена поточно-безопасным способом с использованием простого стека связанных списков; когда событие будет поднято, код может после Interlocked.Exchange отменить порядок элементов стека. Для метода отмены подписки я бы предложил просто установить флаг в элементе списка-вызовы. Это может в принципе вызвать утечку памяти, если события были неоднократно подписаны и отписаны без события, когда-либо возникавшего, но это сделало бы для очень легкого потокобезопасного метода отмены подписки. Если кто-то хотел избежать утечки памяти, можно было бы подсчитать количество незарегистрированных событий в списке; если в списке добавлено слишком много отложенных событий, когда делается попытка добавить новый, метод добавления может пройти через список и удалить их. Все еще работоспособно в полностью незащищенном потокобезопасном коде, но сложнее.