asynchronous - async* 是否懒惰地调用该函数?
问题描述
在为我的项目编写一些单元测试时,我遇到了一个有趣的问题。
这是用户可以用来放置标记的地图:
class DomainMap {
static const _DEFAULT_COORDINATE = const Coordinate(40.73, -73.93);
final ReverseGeocodingStrategy _geocodingStrategy;
final RouteDefinitionStrategy _assemblyStrategy;
final List<_IdentifiedCoordinate> _addressed = [];
final List<Coordinate> _markers = [];
final _Route _route = _Route();
Coordinate get defaultCoordinate => _DEFAULT_COORDINATE;
DomainMap(this._geocodingStrategy, this._assemblyStrategy);
Stream<MarkersUpdateEvent> mark(Coordinate coordinate) async* {
_markers.add(coordinate);
yield _assembleMarkersUpdate();
final Address address = await _geocodingStrategy.geocode(coordinate);
_addressed.add(_IdentifiedCoordinate(coordinate, address));
if (_addressed.length > 1) {
final Iterable<Coordinate> assembledPolyline =
await _assemblyStrategy.buildRoute(BuiltList(_addressed
.map((identifiedCoordinate) => identifiedCoordinate.address)));
assembledPolyline.forEach(_route.add);
yield _assembleMarkersUpdate();
}
}
MarkersUpdateEvent _assembleMarkersUpdate() =>
MarkersUpdateEvent(BuiltList.from(_markers), _route.readOnly);
}
class _Route {
final List<Coordinate> _points = [];
Iterable<Coordinate> get readOnly => BuiltList(_points);
void add(final Coordinate coordinate) => _points.add(coordinate);
void addAll(final Iterable<Coordinate> coordinate) => _points.addAll(coordinate);
}
这是一个单元测试,它检查这里的第二个标记是否应该是返回的路由:
test("mark, assert that on second mark at first just markers update is published, and then the polyline update too", () async {
final Coordinate secondCoordinate = plus(givenCoordinate, 1);
final givenRoute = [
givenCoordinate,
minus(givenCoordinate, 1),
plus(givenCoordinate, 1)
];
when(geocodingStrategy.geocode(any)).thenAnswer((invocation) => Future.value(Address(invocation.positionalArguments[0].toString())));
when(assemblyStrategy.buildRoute(any))
.thenAnswer((_) => Future.value(givenRoute));
final expectedFirstUpdate =
MarkersUpdateEvent([givenCoordinate, secondCoordinate], []);
final expectedSecondUpdate =
MarkersUpdateEvent([givenCoordinate, secondCoordinate], givenRoute);
final DomainMap map = domainMap();
map.mark(givenCoordinate)
//.forEach(print) //Important
;
expect(map.mark(secondCoordinate),
emitsInOrder([expectedFirstUpdate, expectedSecondUpdate]));
}, timeout: const Timeout(const Duration(seconds: 10)));
当我这样运行它时,测试失败并说流只发出一个值 - 一个更新事件,只有一个markers
字段不为空,它只包含一个secondCoordinate
. 但是当我取消注释时forEach
,测试通过了。
据我了解 -async*
方法不会被调用,直到流的值不会被请求,所以当forEach
被调用时 - 函数被执行到最后。因此,如果我请求所有流的(从第一次调用返回的)值 - 方法得到执行,markers
列表填充,第二次执行在预期状态下执行。
我async*
是否正确理解语义?这里有没有一种方法可以让这个函数变得急切而不是懒惰(我不想请求不需要的流的值)?
解决方案
是的,在您调用返回的流async*
之后会延迟调用该函数。listen
如果你从不听,那么什么都不会发生。它甚至是异步的,而不是直接响应listen
调用。
因此,如果您确实需要发生某些事情,但可能只需要查看响应,那么您就不能使用async*
函数来做某事。
您可能想要做的是有条件地填充流,但前提是实际收听了流。这是一个不匹配async*
甚至async
语义不匹配的非传统操作序列。您必须为操作完成做好准备,然后再收听流。这建议将操作分成两部分,一部分async
用于请求,另一部分async*
用于响应,并在两者之间共享未来,这意味着两次侦听相同的未来,这是一种独特的非async
行为。
我建议将流行为分开并使用 a StreamController
。
Stream<MarkersUpdateEvent> mark(Coordinate coordinate) {
var result = StreamController<MarkersUpdateEvent>();
() async {
_markers.add(coordinate);
result.add(_assembleMarkersUpdate());
final Address address = await _geocodingStrategy.geocode(coordinate);
_addressed.add(_IdentifiedCoordinate(coordinate, address));
if (_addressed.length > 1) {
final Iterable<Coordinate> assembledPolyline =
await _assemblyStrategy.buildRoute(BuiltList(_addressed
.map((identifiedCoordinate) => identifiedCoordinate.address)));
assembledPolyline.forEach(_route.add);
result.add(_assembleMarkersUpdate());
}
result.close();
}().catchError(result.addError);
return result.stream;
}
这样,程序逻辑的运行与是否有人收听流无关。您仍然可以缓冲所有流事件。没有真正的方法可以避免这种情况,除非您可以在以后计算它们,因为您不知道何时有人可能会收听返回的流。它不必在返回时立即发生。