摘要
首先要指出的是,您实际上不需要使用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)
的模式,但实际上事件可以使用
任何委托类型(甚至带有返回类型的委托)。我们通过将适当的委托传递到特殊函数中(通常是通过+=运算符)将其添加到委托链中来订阅事件,或者如果尚未订阅任何处理程序,则该委托成为链的根。这就是为什么在引发事件时必须进行空值检查的原因。
当事件被触发时,(通常)委托链被调用,以便依次调用链中的每个委托。要取消订阅.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>
接口的对象来形成对Observable的订阅,这个接口具有OnNext
、OnCompleted
和OnError
方法,由Observable调用它们来处理事件。此外,Subscribe
方法返回一个IDisposable
句柄,可以被释放以取消订阅。
更典型的情况是,我们使用方便的扩展方法来重载Subscribe
。这些扩展接受符合OnXXX
签名的委托处理程序,并透明地创建一个AnonymousObservable<T>
,其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)
- 取消订阅,例如假设我们将从
Subscribe
调用中保留返回的IDisposable
句柄到名为subscription
的变量中,那么对subscription.Dispose()
的调用将映射到fooInstance.BarEvent -= barHandlerInstance
。
请注意,只有调用Subscribe
才会创建订阅。因此,Observable.FromEvent
调用返回一个支持对底层事件进行订阅、调用和取消订阅的工厂。在这一点上,没有事件订阅发生。只有在调用Subscribe
时,观察者才可用,以及它的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事件调用的响应中使用字符串值进行调用。工厂函数创建了一个BarHandler类型的委托处理程序,用于处理通过调用onNextHandler并使用对应事件调用的参数创建的格式化字符串的事件调用。
通过一些类型推断,我们可以将上述代码折叠成以下等效代码:
onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y)
因此,转换函数在提供创建适当事件处理程序的功能方面实现了一些事件订阅逻辑,并且还执行将.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事件进行单个订阅。然后通过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
我希望这篇文章能够帮助你更好地理解这个复杂的函数是如何工作的!
FromEvent
存在的目的是处理所有委托类型,包括那些不符合标准 .NET 事件模式的类型。如果该委托恰好是 Action<T>,则无需进行转换。然而,FromEventPattern
仅适用于传统事件委托。 - James WorldFromEvent(...)
时,如果出现诸如System.Reflection.TargetInvocationException
和System.Reflection.TargetParameterCountException
等嵌入式异常,则应编写转换处理程序并返回与事件使用的相同组合((sender, args)
)的Observable,或者使用FromEventPattern
。 - this.myself