Подтвердить что ты не робот

Как использовать Observable.FromEvent вместо FromEventPattern и избегать имен строковых литералов

Я изучаю свой путь вокруг Rx в WinForms и имею следующий код:

// Create an observable from key presses, grouped by the key pressed
var groupedKeyPresses = Observable.FromEventPattern<KeyPressEventArgs>(this, "KeyPress")
                                  .Select(k => k.EventArgs.KeyChar)
                                  .GroupBy(k => k);

// Increment key counter and update user display
groupedKeyPresses.Subscribe(keyPressGroup =>
{
    var numPresses = 0;
    keyPressGroup.Subscribe(key => UpdateKeyPressStats(key, ++numPresses));
});

Это работает/отлично работает, потоки в событиях KeyPress, группах нажатием клавиши, а затем отслеживает, сколько раз каждый ключ был нажат, и вызывает метод UpdateKeyPressStats с ключом и новым числом нажатий. Отправляй это!

Тем не менее, я не являюсь поклонником подписи FromEventPattern, из-за ссылки на литералы по строкам. Итак, я решил, что попробую FromEvent.

// Create an observable from key presses, grouped by the key pressed
var groupedKeyPresses = Observable.FromEvent<KeyPressEventHandler, KeyPressEventArgs>(h => this.KeyPress += h, h => this.KeyPress -= h)
                                  .Select(k => k.KeyChar)
                                  .GroupBy(k => k);

// Increment key counter and update user display
groupedKeyPresses.Subscribe(keyPressGroup =>
{
    var numPresses = 0;
    keyPressGroup.Subscribe(key => UpdateKeyPressStats(key, ++numPresses));
});

Итак, единственное изменение заключалось в замене Observable.FromEventPattern на Observable.FromEvent (и путь в запросе Select LINQ для получения KeyChar). Остальные, включая методы Subscribe, идентичны. Однако во время выполнения со вторым решением я получаю:

Необработанное исключение типа "System.ArgumentException" произошло в mscorlib.dll

Дополнительная информация: не может привязываться к целевому методу, поскольку его прозрачность подписи или безопасности несовместима с таковой тип делегата.

Что вызывает это исключение во время выполнения и как его избежать?

  • GUI: WinForms
  • Rx и Rx-WinForms Версия: 2.1.30214.0 (через Nuget)
  • Целевая структура: 4.5
4b9b3361

Ответ 1

Резюме

Первое, что нужно сделать, это не использовать Observable.FromEvent, чтобы избежать ссылки на строковый литерал. Эта версия FromEventPattern будет работать:

var groupedKeyPresses =
    Observable.FromEventPattern<KeyPressEventHandler, KeyPressEventArgs>(
        h => KeyPress += h,
        h => KeyPress -= h)
        .Select(k => k.EventArgs.KeyChar)
        .GroupBy(k => k);

Если вы хотите сделать работу FromEvent, вы можете сделать это следующим образом:

var groupedKeyPresses =
    Observable.FromEvent<KeyPressEventHandler, KeyPressEventArgs>(
        handler =>
        {
            KeyPressEventHandler kpeHandler = (sender, e) => handler(e);
            return kpeHandler;
        }, 
        h => KeyPress += h,
        h => KeyPress -= h)
        .Select(k => k.KeyChar)
        .GroupBy(k => k);

Почему? Это связано с тем, что существует оператор FromEvent для работы с любым типом делегирования событий.

Первый параметр здесь - это функция преобразования, которая соединяет событие с подписчиком Rx. Он принимает обработчик OnNext наблюдателя (a Action<T>) и возвращает обработчик, совместимый с основным делегатом события, который будет вызывать этот обработчик OnNext. Затем этот обработчик может быть подписан на событие.

Мне никогда не нравилась официальная документация MSDN для этой функции, поэтому здесь представлено расширенное объяснение, которое проходит через использование этой функции по частям.

Приостановление на Observable.FromEvent

Ниже описано, почему существует FromEvent и как он работает:

Обзор работы подписок .NET.

Рассмотрим, как работают .NET-события. Они реализуются как цепочки делегатов. Стандартные делегаты событий следуют шаблону delegate void FooHandler(object sender, EventArgs eventArgs), но в действии события могут работать с любым типом делегата (даже с типом возврата!). Мы подписываемся на событие, передавая соответствующий делегат специальной функции, которая добавляет ее в цепочку делегатов (как правило, через оператор + =), или если ни один обработчик не подписан, делегат становится корнем этой цепочки. Вот почему мы должны делать нулевую проверку при создании события.

При возникновении события (обычно) цепочка делегатов вызывается так, чтобы каждый делегат в цепочке вызывался поочередно. Чтобы отказаться от подписки на событие .NET, делегат передается в специальную функцию (обычно с помощью оператора - =), так что ее можно удалить из цепочки делегатов (цепочка продолжается до тех пор, пока не будет найдена совпадающая ссылка, и эта ссылка удалены из цепочки).

Позвольте создать простую, но нестандартную реализацию .NET. Здесь я использую менее распространенный синтаксис add/remove, чтобы выявить базовую цепочку делегатов и позволить нам регистрировать подписку и отменять подписку. В нашем нестандартном событии есть делегат с параметрами целого и строкой, а не с обычным подклассом object sender и EventArgs:

public delegate void BarHandler(int x, string y);

public class Foo
{  
    private BarHandler delegateChain;

    public event BarHandler BarEvent
    {
        add
        {
            delegateChain += value;                
            Console.WriteLine("Event handler added");
        }
        remove
        {
            delegateChain -= value;
            Console.WriteLine("Event handler removed");
        }
    }

    public void RaiseBar(int x, string y)
    {
        var temp = delegateChain;
        if(temp != null)
        {
            delegateChain(x, y);
        }
    }
}

Обзор работы подписок Rx

Теперь рассмотрим, как работают наблюдаемые потоки. Подписка на наблюдаемый формируется путем вызова метода Subscribe и передачи объекта, реализующего интерфейс IObserver<T>, который имеет методы OnNext, OnCompleted и OnError, вызываемые наблюдаемым для обработки событий. Кроме того, метод Subscribe возвращает дескриптор IDisposable, который может быть удален для отказа от подписки.

Более типично мы используем методы расширения удобства, которые перегружают Subscribe. Эти расширения принимают обработчики делегатов, соответствующие сигнатурам OnXXX и прозрачно создают AnonymousObservable<T>, методы OnXXX будут вызывать эти обработчики.

Преодоление событий .NET и Rx

Итак, как мы можем создать мост для расширения событий .NET в Rx наблюдаемые потоки? Результатом вызова Observable.FromEvent является создание IObservable, метод Subscribe действует как factory, который создаст этот мост.

Шаблон событий .NET не имеет представления о завершенных или ошибочных событиях. Только о событии. Другими словами, мы должны только соединить три аспекта события, которые отображаются в Rx следующим образом:

  • Подписка, например, вызов IObservable<T>.Subscribe(SomeIObserver<T>) соответствует fooInstance.BarEvent += barHandlerInstance.
  • Вызов, например. вызов barHandlerInstance(int x, string y) отображается на SomeObserver.OnNext(T arg)
  • Отмена подписки, например. что мы сохраняем возвращенный обработчик IDisposable из нашего вызова Subscribe в переменную с именем subscription, тогда вызов subscription.Dispose() отображается в fooInstance.BarEvent -= barHandlerInstance.

Обратите внимание, что это только тот акт вызова Subscribe, который создает подписку. Таким образом, вызов Observable.FromEvent возвращает factory поддержку подписки, вызов и отмену подписки из основного события. На данный момент подписки на события нет. Только в точке вызова Subscribe будет доступен Наблюдатель вместе с ним обработчик OnNext. Поэтому вызов FromEvent должен принимать методы factory, которые он может использовать для реализации трех мостовых действий в соответствующее время.

Аргументы типа FromEvent

Итак, теперь рассмотрим правильную реализацию FromEvent для вышеуказанного события.

Вспомним, что обработчики OnNext принимают только один аргумент. Обработчики событий .NET могут иметь любое количество параметров. Поэтому наше первое решение - выбрать один тип для представления вызовов событий в целевом наблюдаемом потоке.

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

Здесь мы сопоставим аргументы int x, string y вызова BarEvent в форматированную строку, описывающую оба значения. Другими словами, мы вызываем вызов fooInstance.RaiseBar(1, "a"), чтобы вызвать вызов someObserver.OnNext("X:1 Y:a").

В этом примере следует упомянуть очень общий источник путаницы: что представляют собой параметры типа FromEvent? Здесь первый тип BarHandler является исходным типом делегата .NET.NET, второй тип - это тип аргумента target OnNext. Поскольку этот второй тип часто является подклассом EventArgs, он часто предполагал, что он должен быть некоторой необходимой частью делегата события .NET. Многие люди упускают из виду, что его релевантность действительно связана с обработчиком OnNext. Итак, первая часть нашего вызова FromEvent выглядит следующим образом:

 var observableBar = Observable.FromEvent<BarHandler, string>(

Функция преобразования

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

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

(Action<string> onNextHandler) =>
{
    BarHandler barHandler = (int x, string y) =>
    {
        onNextHandler("X:" + x + " Y:" + y);
    };
    return barHandler;
}

Таким образом, эта функция преобразования является функцией factory, которая при вызове создает обработчик, совместимый с основным событием .NET. Функция factory принимает делегат OnNext. Этот делегат должен быть вызван возвращаемым обработчиком в ответ на функцию обработчика, вызываемую с помощью основных аргументов .NET. Делегат будет вызван с результатом преобразования аргументов события .NET в экземпляр типа параметра OnNext. Поэтому из приведенного выше примера видно, что функция factory будет вызываться с onNextHandler типа Action<string> - она ​​должна быть вызвана со строковым значением в ответ на каждый вызов события .NET. Функция factory создает обработчик делегата типа BarHandler для события .NET, который обрабатывает вызовы событий, вызывая onNextHandler с форматированной строкой, созданной из аргументов соответствующего вызова события.

С небольшим типом вывода мы можем свернуть приведенный выше код до следующего эквивалентного кода:

onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y)

Таким образом, функция преобразования выполняет некоторую логику Subscription для предоставления функции для создания соответствующего обработчика событий, а также выполняет работу по привязке вызова события .NET к вызову обработчика Rx OnNext.

Как упоминалось ранее, существуют перегрузки FromEvent, которые опускают функцию преобразования. Это связано с тем, что это не требуется, если делегат события уже совместим с сигнатурой метода, требуемой для OnNext.

Обработчики добавления/удаления

Остальными двумя аргументами являются addHandler и removeHandler, которые отвечают за подписку и отписку созданного обработчика делегата с реальным событием .NET. Предполагая, что у нас есть экземпляр Foo, называемый Foo, то завершенный вызов FromEvent выглядит следующим образом:

var observableBar = Observable.FromEvent<BarHandler, string>(
    onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y),
    h => foo.BarEvent += h,
    h => foo.BarEvent -= h);

Нам нужно решить, как будет происходить событие, которое мы собираемся провести, - поэтому мы предоставляем функции добавления и удаления обработчиков, которые ожидают предоставления созданного обработчика преобразования. Событие обычно захватывается с помощью замыкания, как в приведенном выше примере, где мы закрываем экземпляр Foo.

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

Еще одна вещь...

Там есть один последний кусок клея. Rx оптимизирует подписки на событие .NET. В действительности, для любого заданного количества подписчиков на наблюдаемые, только одна подписка делается на основное событие .NET. Затем это многоадресное соединение с абонентами Rx через механизм Publish. Это как если бы Publish().RefCount() был добавлен к наблюдаемому.

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

public static void Main()
{
    var foo = new Foo();

    var observableBar = Observable.FromEvent<BarHandler, string>(
        onNextHandler => (int x, string y)
            => onNextHandler("X:" + x + " Y:" + y),
    h => foo.BarEvent += h,
    h => foo.BarEvent -= h);

    var xs = observableBar.Subscribe(x => Console.WriteLine("xs: " + x));
    foo.RaiseBar(1, "First");    
    var ys = observableBar.Subscribe(x => Console.WriteLine("ys: " + x));
    foo.RaiseBar(1, "Second");
    xs.Dispose();
    foo.RaiseBar(1, "Third");    
    ys.Dispose();
}

Это производит следующий вывод, демонстрируя только одну подписку:

Event handler added
xs: X:1 Y:First
xs: X:1 Y:Second
ys: X:1 Y:Second
ys: X:1 Y:Third
Event handler removed

Я помогаю этому, помогает устранить любую затяжную путаницу в отношении того, как работает эта сложная функция!