假设我有以下Observable
。 (请注意,解析逻辑位于不同的层中,并且应该是可测试的,因此它必须保持单独的方法。还要注意实际循环是解析XML并具有各种分支和异常处理)。
IObservable<string> GetLinesAsync(StreamReader r)
{
return Observable.Create<string>(subscribeAsync: async (observer, ct) =>
{
//essentially force a continuation/callback to illustrate my problem
await Task.Delay(5);
while (!ct.IsCancellationRequested)
{
string readLine = await r.ReadLineAsync();
if (readLine == null)
break;
observer.OnNext(readLine);
}
});
}
我想使用此功能,例如使用另一个产生Observable
的{{1}},如下所示,但无论如何我都无法处理。
StreamReader
如果你运行这几次(例如使用断点等),你应该会看到它因为TextReader关闭而爆炸。 (在我的实际问题中,它偶尔会在[TestMethod]
public async Task ReactiveTest()
{
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini");
var source1 = Observable.Using(
() => File.OpenRead(filePath),
readFile => Observable.Using(() => new StreamReader(readFile),
reader => Observable.Return(reader)
)
);
//"GetLinesAsync" already exists. How can I use it?
var combined = source1
.SelectMany(GetLinesAsync);
int count = await combined.Count();
}
上发生,但ReadLineAsync
会让它更容易发生。显然,异步性质会导致第一个observable处理流,并且只有在此之后才会发生延续,当然此时流已经关闭。
所以:
Task.Delay
已经存在,如果可能,则不应更改其签名(例如,接收GetLinesAsync
)IObservable<StreamReader>
一起使用?*这是我设置第一个可观察的
的另一种方式async
答案 0 :(得分:2)
您真的需要在这里充分利用Defer
和Using
运算符。
Using
专门针对您希望创建并在订阅开始和完成时最终处理的可支配资源的情况。
Defer
是一种确保您在新订阅时始终创建新管道的方法(read more on MSDN)
你的第二种方法是要走的路。你有100%的权利:
Observable.Using(
() => File.OpenRead(filePath),
readFile =>
Observable.Using(
() => new StreamReader(readFile),
reader =>
这将在每个正确的时间打开并处理资源。
这是代码块之前的内容以及需要修复的reader =>
之后的内容。
reader =>
之后:
Observable
.Defer(() => Observable.FromAsync(() => reader.ReadLineAsync()))
.Repeat()
.TakeWhile(x => x != null)));
这是Rx从流中读取直到完成的惯用方法。
“之前”块只是另一个Defer
,以确保您为每个新订阅者计算Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini")
。在这种情况下没有必要,因为我们知道filePath
不会改变,但这是一种很好的做法,而且当这个值发生变化时非常重要。
这是完整的代码:
public async Task ReactiveTest()
{
IObservable<string> combined =
Observable.Defer(() =>
{
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini");
return
Observable.Using(
() => File.OpenRead(filePath),
readFile =>
Observable.Using(
() => new StreamReader(readFile),
reader =>
Observable
.Defer(() => Observable.FromAsync(() => reader.ReadLineAsync()))
.Repeat()
.TakeWhile(x => x != null)));
});
int count = await combined.Count();
}
我已经对它进行了测试,效果非常好。
鉴于你有GetLines
的固定签名,你可以这样做:
public IObservable<string> GetLines(StreamReader reader)
{
return Observable
.Defer(() => Observable.FromAsync(() => reader.ReadLineAsync()))
.Repeat()
.TakeWhile(x => x != null);
}
public async Task ReactiveTest()
{
IObservable<string> combined =
Observable.Defer(() =>
{
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini");
return
Observable.Using(
() => File.OpenRead(filePath),
readFile =>
Observable.Using(
() => new StreamReader(readFile),
GetLines));
});
int count = await combined.Count();
}
它也有效并经过测试。
答案 1 :(得分:1)
您遇到的问题是您的序列会返回一个项目,即读者。利用阅读器,需要打开文件流。遗憾的是,文件流在创建流阅读器后立即关闭:
StreamReader reader
已创建OnNext(reader)
被称为using
阻止退出,处理流糟糕!
要解决此问题,您必须将StreamReader
的生命周期与使用者的生命周期联系起来,而不是生产者。发生原始错误是因为Observable.Using
在源上调用OnCompleted
后立即处置资源。
// Do not dispose of the reader when it is created
var readerSequence = Observable.Return(new StreamReader(ms));
var combined = readerSequence
.Select(reader =>
{
return Observable.Using(() => reader, resource => GetLines(resource));
})
.Concat();
我不是一个忠实的粉丝,因为你现在依靠你的消费者来清理每个StreamReader
,但我还没有制定更好的方法!
答案 2 :(得分:0)
到目前为止,这是允许我继续使用GetLinesAsync的唯一有效的方法:
//"GetLinesAsync" already exists. How can I use it?
[TestMethod]
public async Task ReactiveTest2()
{
var combined2 = Observable.Create<string>(async observer =>
{
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini");
using (FileStream readFile = File.OpenRead(filePath))
{
using (StreamReader reader = new StreamReader(readFile))
{
await GetLinesAsync(reader)
.ForEachAsync(result => observer.OnNext(result));
}
}
});
int count = await combined2.Count();
}
这不能可靠地运作:
[TestMethod]
public async Task ReactiveTest3()
{
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini");
var source1 = Observable.Defer(() => Observable.Using(
() => File.OpenRead(filePath),
readFile => Observable.Using(() => new StreamReader(readFile),
reader => Observable.Return(reader)
)
));
//"GetLines" already exists. How can I use it?
var combined = source1
.SelectMany(reader => Observable.Defer(() => GetLinesAsync(reader)));
int count = await combined.Count();
}
根据Enigmativity的解决方案,只有一个Observable似乎才有效:
[TestMethod]
public async Task ReactiveTest4()
{
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "win.ini");
var source1 = Observable.Using(
() => File.OpenRead(filePath),
readFile => Observable.Using(() => new StreamReader(readFile),
reader => GetLinesAsync(reader)
)
);
int count = await source1.Count();
}
但是我还没有找到一种方法来保持两个Observable(我用于分层和单元测试目的)之间的分离并让它正常工作,所以我不会考虑回答这个问题。