我正在WinForms中学习Rx,并有以下代码:
// 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's 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's display
groupedKeyPresses.Subscribe(keyPressGroup =>
{
var numPresses = 0;
keyPressGroup.Subscribe(key => UpdateKeyPressStats(key, ++numPresses));
});
因此,唯一的更改是用Observable.FromEvent
替换Observable.FromEventPattern
(以及Select
LINQ查询中获得KeyChar
的路径)。其余的方法,包括Subscribe
方法都是相同的。然而,在运行时,我得到了第二个解决方案:
中发生类型为"System.ArgumentException"的未处理异常mscorlib.dll
附加信息:无法绑定到目标方法,因为它签名或安全透明性与委托类型。
是什么导致了此运行时异常,我应该如何避免它?
- GUI:WinForms
- Rx&Rx WinForms版本:2.1.30214.0(通过Nuget)
- 目标框架:4.5
摘要
需要说明的第一点是,实际上不需要使用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订阅者的转换函数。它接受观察者(Action<T>
)的OnNext处理程序,并返回一个与将调用该OnNext处理器的底层事件委托兼容的处理程序。然后可以将生成的处理程序订阅到该事件。
我从来都不喜欢这个函数的官方MSDN文档,所以这里有一个扩展的解释,详细介绍了这个函数的用法。
Observable.FromEvent的下降
以下是FromEvent
存在的原因及其工作方式:
查看.NET事件订阅的工作方式
考虑一下.NET事件是如何工作的。这些被实现为委托链。标准事件委托遵循delegate void FooHandler(object sender, EventArgs eventArgs)
的模式,但实际上事件可以使用任何委托类型(甚至是具有返回类型的委托!)。我们通过将适当的委托传递给一个特殊函数来订阅事件,该函数将事件添加到委托链中(通常通过+=运算符),或者,如果尚未订阅任何处理程序,则委托将成为链的根。这就是为什么我们在引发事件时必须进行null检查的原因。
当引发事件时,(通常)会调用委托链,以便依次调用链中的每个委托。要取消订阅.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订阅的工作方式
现在考虑一下Observable流是如何工作的。通过调用Subscribe
方法并传递实现IObserver<T>
接口的对象来形成对可观察对象的订阅,该接口具有可观察对象调用的OnNext
、OnCompleted
和OnError
方法来处理事件。此外,Subscribe
方法返回一个IDisposable
句柄,该句柄可以被释放以取消订阅。
更典型的情况是,我们使用方便的扩展方法来重载Subscribe
。这些扩展接受符合OnXXX
签名的委托处理程序,并透明地创建AnonymousObservable<T>
,其OnXXX
方法将调用这些处理程序。
桥接.NET和Rx事件
那么,我们如何创建一个桥梁来将.NET事件扩展到Rx可观察流中呢?调用Observable.FromEvent的结果是创建一个IOobservable,它的Subscribe
方法就像一个将创建此桥的工厂。
.NET事件模式没有完成事件或错误事件的表示形式。仅针对正在引发的事件。换句话说,我们必须只桥接映射到Rx的事件的三个方面,如下所示:
- 订阅例如,对
IObservable<T>.Subscribe(SomeIObserver<T>)
的调用映射到fooInstance.BarEvent += barHandlerInstance
- 调用例如,对
barHandlerInstance(int x, string y)
的调用映射到SomeObserver.OnNext(T arg)
- 取消订阅例如,假设我们将
Subscribe
调用中返回的IDisposable
处理程序保留到一个名为subscription
的变量中,则对subscription.Dispose()
的调用映射到fooInstance.BarEvent -= barHandlerInstance
请注意,只有调用Subscribe
的行为才能创建订阅。因此,Observable.FromEvent
调用返回一个工厂,支持对底层事件的订阅、调用和取消订阅。在这一点上,没有发生事件订阅。只有在调用Subscribe
时,Observer及其OnNext
处理程序才可用。因此,FromEvent
调用必须接受它可以在适当的时间用来实现三个桥接操作的工厂方法。
FromEvent类型参数
因此,现在让我们考虑针对上述事件的FromEvent
的正确实现。
回想一下,OnNext
处理程序只接受一个参数。NET事件处理程序可以具有任意数量的参数。因此,我们的第一个决定是选择一种类型来表示目标可观察流中的事件调用。
事实上,这可以是您希望出现在目标可观察流中的任何类型。转换函数(稍后将讨论)的工作是提供将事件调用转换为OnNext调用的逻辑,并且有很大的自由度来决定如何进行。
在这里,我们将BarEvent调用的int x, string y
参数映射到一个格式化的字符串中,该字符串描述这两个值。换句话说,我们将引起对fooInstance.RaiseBar(1, "a")
的调用,从而导致对someObserver.OnNext("X:1 Y:a")
的调用。
这个例子应该消除一个非常常见的混淆来源:FromEvent
的类型参数代表什么?这里,第一种类型BarHandler
是源.NET事件委托类型,第二种类型是目标OnNext
处理程序的参数类型因为第二种类型通常是EventArgs
子类,所以通常认为它必须是.NET事件委托的某个必要部分——很多人忽略了它的相关性实际上是由于OnNext
处理程序造成的。因此,我们的FromEvent
调用的第一部分看起来是这样的:
var observableBar = Observable.FromEvent<BarHandler, string>(
转换函数
现在让我们考虑FromEvent
的第一个参数,即所谓的转换函数。(注意,FromEvent
的一些重载省略了转换功能,稍后将对此进行详细介绍。)
由于类型推断,lambda语法可能会被截断很多,因此这里有一个长期版本:
(Action<string> onNextHandler) =>
{
BarHandler barHandler = (int x, string y) =>
{
onNextHandler("X:" + x + " Y:" + y);
};
return barHandler;
}
因此,这个转换函数是一个工厂函数,当调用时,它会创建一个与底层.NET事件兼容的处理程序。工厂功能接受OnNext
委派。此委托应由返回的处理程序调用,以响应使用基础.NET事件参数调用的处理程序函数。将调用委托,其结果是将.NET事件参数转换为OnNext
参数类型的实例。因此,从上面的例子中,我们可以看到工厂函数将使用类型为Action<string>
的onNextHandler
进行调用——它必须使用字符串值来调用,以响应每次.NET事件调用。工厂函数为.NET事件创建一个类型为BarHandler
的委托处理程序,该处理程序通过使用根据相应事件调用的参数创建的格式化字符串来调用onNextHandler
来处理事件调用。
通过一点类型推断,我们可以将上面的代码折叠为以下等效代码:
onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y)
因此,转换函数在提供创建适当的事件处理程序的函数时满足了事件订阅逻辑的部分,它还完成了将.NET事件调用映射到RxOnNext
处理程序调用的工作。
如前所述,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事件进行一次订阅。然后经由Publish
机制将其多播到Rx订户。就好像一个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
我确实帮助这有助于消除对这个复杂函数如何工作的任何挥之不去的困惑!
要避免字符串形式的事件名称,请使用nameof
运算符:
var keyPresses = Observable.FromEventPattern<KeyPressEventArgs>(this, nameof(KeyPress))
.Select(k => k.EventArgs.KeyChar)
.GroupBy(k => k);
而不是像那样对事件名称进行硬编码
var keyPresses = Observable.FromEventPattern<KeyPressEventArgs>(this, "KeyPress")
.Select(k => k.EventArgs.KeyChar)
.GroupBy(k => k);
请注意,nameof
运算符可用于C#或更高版本。