Написание функции, которая должна ждать обратного вызова из события

#c# #.net #events

#c# #.net #Мероприятия

Вопрос:

Проблема

Рассмотрим следующий код:

 class A
{
    private int _workId;

    public void DoWork()
    {
        _workId = new Random().Next(100);
        Console.WriteLine("Starting a long process ID: "   _workId);
        LongProcess longProcess = new LongProcess();
        longProcess.StartWork();
        longProcess.OnWorkMiddle  = OnLongProcessWorkMiddle;
        Console.WriteLine("At end of long process: "   _workId);
        longProcess.OnWorkMiddle -= OnLongProcessWorkMiddle;
    }

    private void OnLongProcessWorkMiddle(object sender, EventArgs e)
    {
        Console.WriteLine("In middle of long process: "   _workId);
    }
}
  

Следующий код, на мой взгляд, проблематичен по нескольким причинам:

  1. Существует разделение кода, который по логике должен быть сгруппирован вместе — я бы ожидал, что Console.WriteLine("In middle of long process: " _workId); он появится рядом с Console.WriteLine("At end of long process: " _workId); , и все же они оба отображаются в отдельных функциях.
  2. _workId по всем правилам должна быть функция внутри DoWork, однако я вынужден сделать ее field functions, чтобы иметь возможность доступа к этой информации изнутри обратного вызова. Каждый дополнительный параметр или возвращаемое значение также станут элементом поля.

Альтернативные решения

Есть несколько решений, которые могли бы уменьшить количество проблем в этом коде:

Инкапсулирование функции и ее обратного вызова в другой класс

Одной из проблем, которые я описал выше, были элементы поля, которые не обязательно связаны с классом owner в целом, но вместо этого специфичны для функции DoWork и ее обратных вызовов. Инкапсулируя функцию в ее собственном классе, мы удаляем этот ненужный беспорядок и группируем его в свою собственную логическую группу. Недостатком является то, что это требует создания класса из того, что, по сути, должно было быть функцией. Это также не решает проблему № 1.

Использование анонимных делегатов для сохранения в одной функции

Мы могли бы переписать код следующим образом:

 public void DoWork()
{
    var workId = new Random().Next(100);

    EventHandler onLongProcessWorkMiddle = delegate { Console.WriteLine("In middle of long process: "   workId); };

    Console.WriteLine("Starting a long process ID: "   workId);
    LongProcess longProcess = new LongProcess();
    longProcess.StartWork();
    longProcess.OnWorkMiddle  = onLongProcessWorkMiddle;
    Console.WriteLine("At end of long process: "   workId);
    longProcess.OnWorkMiddle -= onLongProcessWorkMiddle;
}
  

Теперь нам больше не нужны какие-либо элементы поля, и мы можем красиво сохранить все в одной функции. Однако с точки зрения удобочитаемости я нахожу код довольно слабым, поскольку код (или, скорее, его делегат), который выполняется позже, появляется сверху.

Использование полу-универсального вспомогательного класса

В этом решении я написал полу-универсальный вспомогательный класс для синхронизации выполнения. Это позволяет мне писать код следующим образом:

 public void DoWork()
{
    var workId = new Random().Next(100);

    LongProcess longProcess = new LongProcess();

    var syncr = new EventSyncr();
    syncr.Sync(
        delegate { longProcess.OnWorkMiddle  = syncr.EventCallback; },
        delegate { longProcess.StartWork(); },
        delegate { Console.WriteLine("In middle of long process: "   workId); },
        delegate { longProcess.OnWorkMiddle -= syncr.EventCallback; });

    Console.WriteLine("At end of long process: "   workId);
}
  

И вот код для вспомогательного класса:

 /// <summary>
/// Used to synchronize code that waits for a callback from a function.
/// </summary>
class EventSyncr
{
    readonly AutoResetEvent _eventCallbackEvent = new AutoResetEvent(false);
    private Action _actionAtEvent;

    /// <summary>
    /// Executes the syncing process. This function is blocked until the event is called back (once), then it executes the 'actionAtEvent' segment and
    /// after unsubscription, exists.
    /// </summary>
    /// <param name="eventSubscription">The passed delegate must subscribe the event to EventSyncr.EventCallback.</param>
    /// <param name="eventCausingAction">The event causing action.</param>
    /// <param name="actionAtEvent">The action at event.</param>
    /// <param name="eventUnsubscription">Unsubscribe the event that was subscribed in the first parameter</param>
    public void Sync(Action eventSubscription, Action eventCausingAction, Action actionAtEvent, Action eventUnsubscription)
    {
        _actionAtEvent = actionAtEvent;

        try
        {
            eventSubscription();
            eventCausingAction();
            _eventCallbackEvent.WaitOne();
        }
        finally
        {
            eventUnsubscription();
        }
    }

    public void EventCallback(object sender, EventArgs e)
    {
        try
        {
            _actionAtEvent();
        }
        finally
        {
            _eventCallbackEvent.Set();
        }
    }
}
  

Хотя это немного многословно, мне кажется, что это решает все проблемы, о которых я говорил выше:

Код отображается именно так, как он будет выполняться, в своем логическом порядке, и все дополнительные свойства являются автономными.

Вопросы

Мне любопытно, что вы думаете о проблеме и возможных решениях. Какой вариант кажется лучшим? Есть ли лучший способ решения проблемы?

Я знаю, что в C # 5 функция await обеспечит гораздо лучшее решение, но мы еще не совсем там : (.

Спасибо!

Ответ №1:

ваш класс EventSyncr настолько странный, что вы должны получить дополнительную награду за это, просто шучу 😉

Но на самом деле вам нужно только создать AutoResetEvent и дождаться его в конце вашего кода, используя WaitHandle .Подождите все.

В конце вашего асинхронного метода вы должны подать сигнал AutoResetEvent, и ваш основной поток узнает, что ваш асинхронный процесс только что завершился.

 public void DoWork()
{
    var workId = new Random().Next(100);

    Console.WriteLine("Starting a long process ID: "   workId);
    LongProcess longProcess = new LongProcess();
    longProcess.StartWork();
    longProcess.OnWorkMiddle  = ()=>{ Console.WriteLine("In middle of long process: "   workId);
    longProcess.OnWorkEnd  = ()=>{ 
       Console.WriteLine("At the end of a long process");
       autoEvent.Set();
    };

    WaitHandle.WaitAll(new []{longProcess.autoEvent});
}
  

Комментарии:

1. @VitalyB у вас появилась идея с вашей функцией «Sync», но то, что вы делаете с делегированием в методах 1 и 2, просто не предназначено для выполнения таким образом. @BitKFu очень хороший ответ! e: Рассмотрите асинхронную потоковую передачу как еще одну хорошую идею 😉

2. Думаю, благодаря комплименту :). Длительный процесс. Start() в коде, который я упомянул, является блокирующей функцией, поэтому я не могу использовать AutoResetEvent для синхронизации потоков. Но вы правы, если бы она выполнялась (и отправляла события) в другом потоке, это было бы жизнеспособным решением. AutoResetEvent в EventSyncr действительно является частью более общего лечения… Возможно, мне следовало бы удалить это, потому что я больше ориентирую вопросы на однопоточные сценарии.

3. Что касается вашего кода — Почему вы не отписались от событий? Это что, ошибка? Или это потому, что вы предполагаете, что ‘longProcess’ в любом случае будет GC’d?