Summary
首先要说明的是,您实际上并不需要使用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 处理程序(Action<T>
)并返回与将调用该 OnNext 处理程序的底层事件委托兼容的处理程序。然后可以订阅该生成的处理程序该事件。
我从来不喜欢该函数的官方 MSDN 文档,所以这里是一个扩展的解释,逐步介绍该函数的用法。
Observable.FromEvent 的内幕
下面分解一下原因FromEvent
存在及其运作方式:
回顾 .NET 事件订阅的工作原理
考虑 .NET 事件如何工作。这些都是作为委托链实现的。标准事件代表遵循以下模式delegate void FooHandler(object sender, EventArgs eventArgs)
,但实际上事件可以与any委托类型(即使是具有返回类型的委托类型!)。我们通过将适当的委托传递给特殊函数来订阅事件,该函数将其添加到委托链(通常通过 += 运算符),或者如果尚未订阅处理程序,则委托将成为链的根。这就是为什么我们在引发事件时必须进行空检查。
引发事件时,(通常)调用委托链,以便依次调用链中的每个委托。要取消订阅 .NET 事件,委托将被传递到一个特殊函数(通常通过 -= 运算符),以便可以将其从委托链中删除(遍历该链,直到找到匹配的引用,并且该链接将被删除)。从链中删除)。
让我们创建一个简单但非标准的 .NET 事件实现。这里我用的是不太常见的添加/删除语法来公开底层委托链并使我们能够记录订阅和取消订阅。我们的非标准事件具有一个带有整数和字符串参数的委托,而不是通常的委托object sender
and 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
and OnError
可观察对象调用的方法来处理事件。另外还有Subscribe
方法返回一个IDisposable
可用于取消订阅的句柄。
更典型的是,我们使用方便的扩展方法来重载Subscribe
。这些扩展接受符合以下要求的委托处理程序OnXXX
签名并透明地创建AnonymousObservable<T>
whose OnXXX
方法将调用这些处理程序。
桥接 .NET 和 Rx 事件
那么我们如何创建一个桥梁来将 .NET 事件扩展到 Rx 可观察流中呢?调用 Observable.FromEvent 的结果是创建一个 IObservable,其Subscribe
方法就像一个创建这座桥的工厂。
.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
调用返回一个工厂,支持订阅、调用和取消订阅底层事件。此时,没有发生任何事件订阅。仅在调用时Subscribe
观察者是否可用,以及它的OnNext
处理程序。因此,FromEvent
call 必须接受可用于在适当时间实现三个桥接操作的工厂方法。
FromEvent 类型参数
现在让我们考虑一下正确的实现FromEvent
对于上述事件。
回想起那个OnNext
处理程序仅接受一个参数。 .NET 事件处理程序可以有any参数数量。因此,我们的第一个决定是选择单一类型来表示目标可观察流中的事件调用。
其实这可以是any您希望出现在目标可观察流中的类型。转换函数(稍后讨论)的工作是提供将事件调用转换为 OnNext 调用的逻辑 - 并且可以自由地决定如何发生这种情况。
在这里我们将绘制地图int x, string y
BarEvent 调用的参数转换为描述这两个值的格式化字符串。换句话说,我们将调用fooInstance.RaiseBar(1, "a")
导致调用someObserver.OnNext("X:1 Y:a")
.
这个例子应该可以消除一个非常常见的混乱根源:什么是类型参数FromEvent
代表?这里是第一种类型BarHandler
is 源 .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;
}
所以这个转换函数是工厂功能当调用时creates与底层 .NET 事件兼容的处理程序。工厂函数接受一个OnNext
代表。该委托应由返回的处理程序调用,以响应使用底层 .NET 事件参数调用的处理程序函数。该委托将被调用,并将 .NET 事件参数转换为OnNext
参数类型。所以从上面的例子我们可以看到工厂函数将被调用onNextHandler
类型的Action<string>
- 必须使用字符串值来调用它以响应每个 .NET 事件调用。工厂函数创建一个类型的委托处理程序BarHandler
对于通过调用来处理事件调用的 .NET 事件onNextHandler
使用从相应事件调用的参数创建的格式化字符串。
通过一些类型推断,我们可以将上面的代码折叠为以下等效代码:
onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y)
因此转换函数满足some事件订阅逻辑提供了一个函数来创建适当的事件处理程序,并且它还负责将 .NET 事件调用映射到 RxOnNext
处理程序调用。
如前所述,存在超载FromEvent
省略转换功能。这是因为如果事件委托已经与所需的方法签名兼容,则不需要OnNext
.
添加/删除处理程序
剩下的两个参数是 addHandler 和 removeHandler,它们负责订阅和取消订阅创建的委托处理程序对实际 .NET 事件的处理 - 假设我们有一个实例Foo
called 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
observable 完全实现订阅、调用和取消订阅。
还有一件事...
最后还要提到一块胶水。 Rx 优化对 .NET 事件的订阅。实际上,对于任意给定数量的可观察对象订阅者,仅对底层 .NET 事件进行一次订阅。然后通过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
我确实有助于消除对这个复杂功能如何工作的任何挥之不去的困惑!