异步
129
以使用
await as
async function
countdown(int $start): AsyncKeyedIterator<int, string> {
for
($i = $start; $i >= 0; --$i) {
await
HH\Asio\usleep(1000000);
yield
$i => (string)$i;
}
}
async function
use_countdown(): Awaitable<void> {
foreach
(countdown(10)
await as
$num => $str) {
// $num
是个整型
// $str
是个字符串型
var_dump($num, $str);
}
}
最后,如果你希望在一个异步生成器上调用
send()
或者
raise()
方法,你需要使用接
AsyncGenerator
作为替代。它拥有三个类型实参:键的类型、值的类型,以及你希
望传递
send()
的类型
async function
namifier(): AsyncGenerator<int, string, int> {
//
得到第一个
id
$id =
yield
0 => '';
// $id
的类型为
int
while
($id !==
null
) {
$name =
await
get_name($id);
$id =
yield
$id => $name;
}
}
async function
use_namifier(
array
<int> $ids): Awaitable<void> {
$namifier = namifier();
await
$namifier->next();
//
注意:这是一个结构性非常差的异步代码!
//
仅用于范例演示。请不要在循环中使用
await
foreach
($ids
as
$id) {
$result =
await
$namifier->send($id);
// $result
的类型是
(int, string)
}
}
有一些非常重要的事情需要指出。第一,虽然
AsyncGenerator
的第三个类型实参是
int
,在异步生成器中,一个
yield
的结果是
type?
int
。这是因为把
null
传递给
send()
是合法的。(这么做和调
next()
果是一样的。)
第二,
await $namifier->send($id)
的结果是类型
?(int, string)
tuple
中包含
yield
的键和值。原因在于这是个
nullable
类型,就是说借助于
yield
break
,生成器能够总是
隐式
yield
null
130
6
第三,请记住在一个异步生成器上,调用
next()
send()
以及
raise()
的时候,你必
须要等待它们,而不仅仅调用它们。
第四,异步迭代器(
AsyncIterator
)和它的伙伴们会从它们的
next()
方法中返回真实
的值,而不是返回
void
(就像非异步迭代器和它的伙伴们所做的那样)。同样的道理在
AsyncGenerator
send()
raise()
方法
上也是一样的。
最后,这个代码仅用于示范目的,而不是这样写异步代码。特别要提醒的是,千万不要
在一个循环中执行等待(见
6.3.2
节的内容)。遗憾的是,到目前为止,关于异步生成
器好的范例代码还很少。因为这里并没有什么扩展使用到它们。当这里有相关代码的时
候,异步生成器将会是非常强大的工具。例如,它们可以被用来从网络服务上实现流结果。
6.2.5
异步函数中的异常
到目前为止,我们所看到的都还是相当简单明了的:当你调用一个异步函数时,它返回
了一个等待句柄。当你等待一个等待句柄,你得到了它的结果:异步函数传递给它的
return
语句的值。但是如果这个异步函数抛出了异常,将会发生什么事情呢?
答案就是,当等待一个等待句柄时,同样的异常对象将会被再次抛出:
async function
thrower(): Awaitable<void> {
throw new
Exception();
}
async function
main(): Awaitable<void> {
//
不会抛出
$handle = thrower();
//
抛出一个异常,和
thrower()
函数一样的异常对象
await
$handle;
}
如果你使用
HH\Asio\v()
或者
HH\Asio\m()
同时等待
多重等待句柄的话,并且其中一个
组件等待句柄抛出一个异常,合并的等待句柄将会再次抛出那个异常。如果多个组件等
待句柄抛出异常,合并的等待句柄将会再次抛出其中的一个。然而,所有的组件等待句
柄将会完成,无论它们是正常完成还是抛出异常:
async function
thrower(string $message): Awaitable<void> {
throw new
Exception($message);
}
async function
main(): Awaitable<void> {
//
不会抛出
$handles = [thrower('one'), thrower('two')];
//
抛出两个异常对象中的一个
异步
131
$results =
await
HH\Asio\v($handles);
}
通常,你并不希望发生异常。但是万一发生这样的情况,你通常希望得到等待句柄成功
的结果值,并忽略掉其余的,或者换一种方式来联络失败。
asio-utilities
提供了一个名叫
HH\Asio\wrap()
的异步函数,它采用一个等待句柄作
为实参。它将会等待你传入的等待句柄,并且捕获它抛出的任何异常信息。如果没有任
何异常发生,它将返回一个包含传入的等待句柄的结果值的对象。如果有异常发生,它
将返回对应的异常对象。它通常以
HH\Asio\ResultOrExceptionWrapper
的形式做这些
事情。
HH\Asio\ResultOrExceptionWrapper
asio-utilities
一个接口,它的定义类似
下面:
namespace
HH\Asio {
interface
ResultOrExceptionWrapper<T> {
public function
isSucceeded(): bool;
public function
isFailed(): bool;
public function
getResult(): T;
public function
getException(): \Exception;
}
}
ResultOrExceptionWrapper
的四个方法是:
isSucceeded()
指明了内部等待句柄是否正常退出。(通过
return
退出)
isFailed()
指明了内部
等待句柄是否非正常退出。(发生了异常)
getResult()
,如果内部等待句柄正常退出,那么返回句柄的结果,否则将会重新抛
出异常。
getException()
,如
果内部等待句柄抛出了异常,那么会返回对应的异常。如果内
部等待句柄没有抛出异常,则该方法将会抛出一个
InvariantException
异常。
这里是一个例子:
async function
thrower(): Awaitable<void> {
throw new
Exception();
}
async
function
wrapped(): Awaitable<void> {
//
不会抛出
$handle = HH\Asio\wrap(thrower());
//
不会抛出
$wrapper =
await
$handle;
132
6
if
($wrapper->isFailed()) {
//
返回
thrower()
函数抛出的相同异常对象
$exc = $wrapper->getException();
}
}
本节中的例子拥有类似下面的代码:
$handle = thrower();
await
$handle;
这个仅仅用于明确,对异步函数的调用不会抛出一个异常,并会正常等待句柄完成。
一般来说,你不应该把这个调用和这样的
await
分离开。
6.5.1
节的相关内容将会对
此详细解释。
6.2.6
映射和过滤助手
当并行地创建了多个等待句柄用于等待的时候,你经常将会有一些值的集合,它们中的
每一个都需要转化为等待句柄,或者你可能需要把它们中的一些过滤出去。你可以使用
平常的
PHP
Hack
的内置函数
array_map()
array_filter()
或者
Hack
集合类上
的方法)来做这些事情,但是这可能会导致你的代码有些冗长。
asio-utilities
供了大量简明的命名函数用于处理异步映射和过滤回调中的数组及集
合。它们都是
vm()
vfk()
mmw()
等这样简明的名字,但是这些函数在异步代码中
很常用,这种易读性的损失所带来的简明性,是非常值得的。
下面将对如何解码这些名字进行详细说明:
第一个字母总
v
或者
m
这表明函数的返回值是什么:一个
Vector
或者一个
Map
接下来,你可能看到了
m
mk
f
或者
fk
。这些意味着集合内的这些值,是否会通
过一个映射(
m
mk
)或者过滤
f
fk
,传递一个回调。如果当前值
k
,这
意味着集合内的键也将会传递到回调中。
最后,这里可能是个
w
。如果是这样的话,在任何的映射或者过滤被执行后,集合
内的值通过
HH\Asio\wrap()
进行传递
第一个参数总是输入的数组或者
collection
。(这个助手实际上接受
Traversable
KeyedTraversable
,视情况而定,所以你也可以传入迭代器。)如果这个函数需要
一个对映射或者过滤的
callback
,这就是第二个实参。(没有哪个函数会需要超过
1
callback
。)
异步
133
映射和过滤的回调是异步函数。映射回调必须一个形参,或者两个形参。当为一个
形参的时候,代表着
collection
value
类型。当为两个形参的时候,则分别代表着
collection
key
value
类型。它们可能返回任何类型。过滤回调有着同样的形参规则,
并且它们必须返回布尔值。
映射是非常常见的:你将会有一个异步函数,它对一个单独的值,做了一个异步操作。
或者你将会在一个值的数组或者集合上进行映射。为了达到这个目的,你可能使
vm()
vmk()
mm()
mmk()
或者在这些函数名称后面带
w
其他函数。这些助手的最基
本操作是:通过传递它到异步回调,给每个值创建一个等待句柄,然后并行等待所有这
些等待句柄,接着把结果值放到一个
Vector
中。这里有个例子向我们展示了:当使用
一个
Vector
以及一个
Map
的时候,将会发生什么事情:
async function
fourth_root(num $f): Awaitable<float> {
if
($f < 0) {
throw new
Exception();
}
return
sqrt(sqrt($f));
}
async function
vector_with_mapping(): Awaitable<void> {
$strs = Vector {16, 81};
$roots =
await
HH\Asio\vm($strs, fun('fourth_root'));
// $roots
Vector {2, 3}
}
async function
map_with_mapping_wrapped(): Awaitable<void> {
$nums = Map {
'minus eighty-one' => -81,
'sixteen' => 16,
};
$roots =
await
HH\Asio\mmw($nums, fun('fourth_root'));
// $roots['minus eighty-one']
是个失败的
ResultOrExceptionWrapper
// $roots['sixteen']
是个结果为
2
成功的
ResultOrExceptionWrapper
}
过滤就不是那么常见了。你将会使用一个结果为布尔型的异步函数,且并行地把它应
用到一个集合的所有元素上。为了达到这个目的,你将会使
vf()
vfk()
mf()
mfk()
或者其中某一个的函数名上再附加一个
w
对于每个助手,最基本的操作是:通
过把它传递给一个异步回调,为每个值创建一个等待句柄。然后对原始数组或者集合进
行过滤,结果值会是个布尔型。例如:
async function
is_user_admin(int $id): Awaitable<bool> {
// ...
}

Get Hack 与HHVM 权威指南 now with O’Reilly online learning.

O’Reilly members experience live online training, plus books, videos, and digital content from 200+ publishers.