我正在学习Rx,并试图使用SerialPort从GPS设备实现“NMEA句子阅读器”。事实上它的GPS数据对问题的重要性不大,所以让我们澄清NMEA格式由线组成,'$'符号代表新条目的开头,所以你得到的“句子”看起来类似于:
$[data for first line goes here]
$[data for second line goes here]
...
连接SerialPort的DataReceived事件非常简单:
var port = new SerialPort("COM3", 4800);
var serialPortSource = Observable.FromEventPattern<
SerialDataReceivedEventHandler,
SerialDataReceivedEventArgs>
(
handler => port.DataReceived += handler,
handler => port.DataReceived -= handler
).Select(e => port.ReadExisting());
这给出了IObservable<string>
,但显然这里返回的字符串不一定是NMEA句子。例如,对于上面的示例数据,可以得到如下序列:
$[data for first line g
oes here]\r\n$[data for
second line goes here]
如何将其正确地转换为一系列实际句子?在IEnumerable
世界中,我可能会从char
s序列开始,并编写与此类似的扩展方法:
public static IEnumerable<string> ToNmeaSentence(
this IEnumerable<char> characters
)
{
var sb = new StringBuilder();
foreach (var ch in characters)
{
if (ch == '$' && sb.Length > 0)
{
yield return sb.ToString();
sb.Clear();
}
sb.Append(ch);
}
}
现在我想知道在Rx中这种操作是否有惯用的方式?
答案 0 :(得分:5)
它与Enumerables的代码完全相同。您使用Subscribe
代替快速枚举,并使用observer.OnNext
代替yield return
。哦,你必须使用Observable.Create
,因为C#没有像Obumevers那样对Observers有语言支持(但是。这不是Rx的失败)。
Enumerables和Observables完全相同。一推,另一推。创建它们的语法略有不同。就是这样。
public static IObservable<string> ToNmeaSentence(
this IObservable<char> characters
)
{
return Observable.Create<string>(observer => {
var sb = new StringBuilder();
return characters.Subscribe(ch => {
if (ch == '$' && sb.Length > 0)
{
observer.OnNext(sb.ToString());
sb.Clear();
}
sb.Append(ch);
});
});
}
我通常不会在这个低级别上编程,但是Observables不会像Enumerables那样复杂化它。当人们第一次学习Enumerables时,很难理解。当人们第一次学习Observables时,很难理解。他们两个都做同样的事情,但一个推,一个拉。除了那个区别之外,两者之间存在1-1的关系。
你认为Rx比Enumerables和LINQ to Objects更复杂是错误的。当你还在学习它时,它就会出现。
答案 1 :(得分:0)
这是一个常见问题。我目前的想法是你需要一个操作符来获取源序列并根据谓词将其分成(连续)窗口。
此运算符可能很有用。
public static IObservable<IObservable<T>> WindowByExclusive<T>(this IObservable<T> input, Func<T, bool> isWindowBoundary)
{
return Observable.Create<IObservable<T>>(o=>
{
var source = input.Publish().RefCount();
var left = source.Where(isWindowBoundary).Select(_=>Unit.Default).StartWith(Unit.Default);
return left.GroupJoin(
source.Where(c=>!isWindowBoundary(c)),
x=>source.Where(isWindowBoundary),
x=>Observable.Empty<Unit>(),
(_,window)=>window)
.Subscribe(o);
});
}
它基于谓词有效地将源序列分解为切片。
您可以首先将您的字符串列表分成char
s序列来使用它。
然后,您可以应用WindowBy
运算符,其谓词为c=='$'
。
这将为您提供两个包含所需数据的窗口。
现在您已将数据分开,然后您可以将每个窗口中的字符加入到string.Join
的字符串中。
Full LinqPad示例
void Main()
{
var data = new List<string>(){@"$[data for first line g", "oes here]\r\n$[data for ", "second line goes here]"};
data.ToObservable()
.SelectMany(s=>s)
.WindowByExclusive(c => c=='$')
.SelectMany(window=>window.ToList().Select(l=>string.Join(string.Empty, l)))
.Where(s=>!string.IsNullOrEmpty(s))
.Dump("WindowByExclusive");
}
// Define other methods and classes here
public static class ObEx
{
public static IObservable<IObservable<T>> WindowByExclusive<T>(this IObservable<T> input, Func<T, bool> isWindowBoundary)
{
return Observable.Create<IObservable<T>>(o=>
{
var source = input.Publish().RefCount();
var left = source.Where(isWindowBoundary).Select(_=>Unit.Default).StartWith(Unit.Default);
return left.GroupJoin(
source.Where(c=>!isWindowBoundary(c)),
x=>source.Where(isWindowBoundary),
x=>Observable.Empty<Unit>(),
(_,window)=>window)
.Subscribe(o);
});
}
}
输出:
<强> WindowByExclusive 强>
[data for first line goes here]
[data for second line goes here]