本节书摘来自华章出版社《Hack与HHVM权威指南》一书中的第1章,第1.4节,作者 Owen Yamauchi,更多章节内容可以访问“华章计算机”公众号查看。
1.4 Hack的类型系统
Hack提供了一系列强有力的方法来描述类型,在PHP最基本的布尔型、整型、字符串型、数组等类型系统的基础上,添加了很多新的方式来结合它们,并且使之更富有表现力。
原始类型
这里有和PHP一样的原始类型:bool、int、float、string、array和resource,这些都是合法有效的Hack类型标注。
在PHP中,有为这些类型专门附加的名字,比如boolean、integer、real和double类型,但是这些在Hack中都不是合法有效的,上段提及的6种类型是Hack类型系统中可以被接受的原始类型。
作为原始类型的有效补充,这里还有其他两种类型:num,可以是整型或者浮点数类型;arraykey,可以是整型,也可以是字符串类型。
对象类型
任何类或者接口的名字(内置或者非内置的)都能够在类型标注中使用。
枚举类型
枚举类型在第3章中会更加全面地阐述,这里我们只需要知道枚举类型就是一组常量值的名字。枚举类型的名字能够用做类型标注。对应的类型标注的合法值就是这个枚举里面的成员。
元组类型
元组是一个把固定数目的不同类型的值进行打包的办法,元组类型最通常的用途就是从函数中返回多个值。
元组类型标准的语法非常简单,就是用括号括住,用逗号分隔的类型列表(可能在列表中出现任何其他类型,但是void类型不会出现)。创建一个元组类型的语法和创建数组类型的array()语法一样,区别在于关键词“array”被“tuple”代替,并且不能有键名key。
例如,函数将会返回一个包含整数值和浮点数的元组类型。
function find_max_and_index(array<float> $nums): (int, float) {
$max = -INF;
$max_index = -1;
foreach ($nums as $index => $num) {
if ($num > $max) {
$max = $num;
$max_index = $index;
}
}
return tuple($max_index, $max);
}
元组类型的表现更像一个受限版的数组类型,你不能修改元组类型的key集合,也就是说,你不能添加或者移除对象。你能够修改一个元组中的值,但是你不能修改它们的类型。你能够通过数组编号的语法读出元组内的值,但是更通常的做法是通过列表分配把元组类型解包,而不是读取某个单独的元素。
从底层上讲,元组类型确实就是数组,如果你把一个元组类型传递给函数is_array(),这个函数将会返回true。
混合型(mixed)
mixed意味着包含null在内的Hack编程语言里面所允许的任何值。
void
void只在函数返回类型中有效,它意味着这个函数什么也没有返回。(在PHP中,如果一个函数什么也不返回的话,那么事实上返回了一个null的值。但是在Hack中,函数返回值返回viod的话会引发一个错误。)
void包含在mixed类型中,也就是说,如果一个函数的返回类型是mixed,那么它什么也不返回是合法的。
this
this只在方法返回类型中有效,对于一个空函数来说,this并不是一个有效的返回类型。这表明这个方法返回了一个类的对象,该对象的类与这个方法调用时所在对象的类相同。
这种类型标注的目的就是允许在有子类的类上进行链式方法调用,链式方法调用是个非常有用的技巧,它们看起来是这样的:
$random = $rng->setSeed(1234)->generate();
为了实现写链式调用,题目中的类必须从那些没有逻辑返回值的方法返回$this,就像这样:
class RNG {
private int $seed = 0;
public function setSeed(int $seed): RNG {
$this->seed = $seed;
return $this;
}
// ...
}
在这个例子中,如果类RNG没有子类,你可以使用RNG作为setSeed()方法的返回类型标注,这不会有任何问题,但是当RNG有子类的时候,就会出问题了。
在接下来的例子中,类型检查器将会报告一个错误。因为方法setSeed()的返回类型是RNG,它认为调用$rng->setSeed(1234)将会返回一个RNG类型,并且在RNG对象上调用generateSpecial()方法是非法的,因为这个方法仅仅定义在子类中。更多$rng变量特别指明的类型(类型检查器知道这个类型是SpecialRNG)已经丢失了。
class SpecialRNG extends RNG {
public function generateSpecial(): int {
// ...
}
}
function main(): void {
$rng = new SpecialRNG();
$special = $rng->setSeed(1234)->generateSpecial();
}
返回类型标注this巧妙地解决了这个问题:
class RNG {
private int $seed = 0;
public function setSeed(int $seed): this {
$this->seed = $seed;
return $this;
}
// ...
}
现在,当类型检查器计算$rng->setSeed(1234)调用的返回类型的时候,this变量标注将告诉它保持箭头左边表达式的类型,这就是说,关于generateSpecial()的链式调用将是合法的。
静态方法也能够使用返回类型this,在这种情况下,它表明它们返回了一个对象,这个对象的类就是这个方法所在的类,就是说,从函数get_called_class()返回的类的名字。满足this类型标注的办法就是返回new static():class ParentClass {
// 这里需要告之类型检查器 'new static()'是合法的
final protected function __construct() {}
public static function newInstance(): this {
return new static();
}
}
class ChildClass extends ParentClass {
}
function main(): void {
ParentClass::newInstance(); //返回一个ParentClass的实例
ChildClass::newInstance(); // 返回一个ChildClass 的实例
}
类型别名
在3.2节中将详细阐述,类型别名是对已经存在的类型重新命名的好办法,你可以使用新名字作为类型标注。
Shape
Shape类型是个非常特别的类型别名,将在3.3节中详细描述,并且它们的名字也能够作为类型标注使用。
Nullable类型
除void和mixed之外的所有类型都能够通过问号前缀符号标注为可为空的。对于?int的类型标注来说,它能够是整型也可以是null类型。mixed类型不能够标注为nullable的原因就是它已经包含了null类型。
Callable类型
虽然PHP允许callable作为一个参数的类型提示,但是Hack并不允许。相反,Hack提供了更为强大的语法,允许你明确指出不仅仅一个值是可调用的,还包括它作为参数值时是什么类型的,以及它返回的是什么类型。
这个类型的语法是一个关键词function,后面是括号括住的参数类型列表,再后面是冒号和返回类型,上述这些再包在一个括号内。这有些像给函数做变量标注的语法;从根本上来说,这就是一个没有函数名和参数名的函数签名。在下面的例子中,$callback就是这样一个函数,它的参数是一个整型和一个字符串类型,返回字符串类型:
function do_some_work(array $items,
(function(int, string): string) $callback): array {
foreach ($items as $index => $value) {
$string_result = $callback($index, $value);
// ...
}
}
满足callable类型标注的callable值有四种情况:闭包、函数、实例方法和静态方法。让我们通过下面的例子加深一下理解:
下面是一个简单的闭包的例子:
function do_some_work((function(int): void) $callback): void {
// ...
}
function main(): void {
do_some_work(function (int $x): void { / ... / });
}
如果要使用一个已经命名的函数作为callable的值,你需要通过一个特别的函数fun()来传递函数名,例如:
function do_some_work((function(int): void) $callback): void {
// ...
}
function f(int $x): void {
// ...
}
function main(): void {
do_some_work(fun('f'));
}
fun()函数的参数必须是一个单引号的字符串字面量,类型检查器会自动查找函数名,并且检测它的参数类型和返回类型,然后把fun()当成它返回了一个正确类型的callable的值。
当使用实例方法作为callable的值时,你必须通过特殊的函数inst_meth()来传递对象及方法名,这点和fun()函数很像,类型检查器将会查找对应名称的方法,并且将inst_meth()当成它返回了正确类型的callable值一样。再次说明一下,方法名必须是单引号的字符串字面量:
function do_some_work((function(int): void) $callback): void {
// ...
}
class C {
public function do_work(int $x): void {
// ...
}
}
function main(): void {}
$c = new C();
do_some_work(inst_meth($c, 'do_work'));
}
使用静态方法也非常相似:把类名和方法名通过函数class_meth()传递即可。方法名必须是个单引号的字符串字面量。类名可以是个单引号的字符串字面量,或者是Hack中特有的::class构造附加在一个非单引号的类名后面。
function do_some_work((function(int): void): $callback): void {
// ...
}
class C {
public static function prognosticate(int $x): void {
// ...
}
}
function main(): void {
do_some_work(class_meth(C::class, 'prognosticate'));
//等价于:
do_some_work(class_meth('C', 'prognosticate'));
}
在运行环境中,ClassName::class是和'ClassName'等价的。
这里还有另外一种办法来创建一个调用实例方法的callable值,这就是meth_call()函数。它将通过调用你传递给它的一个实例对象上的方法,来创建一个可调用的值。这里有个局限性,就是,这个方法必须没有任何参数,这个局限性在未来的版本中将被解除。
class C {
function speak(): void {
echo "hi!";
}
}
function main(): void {
$caller = meth_caller(C::class, 'speak');
$obj = new C();
$caller($obj); //等价于调用$obj->speak();
}
与能够打包一个特定对象和调用它的方法的函数inst_meth()相比,meth_caller()函数与array_map()和array_filter()这样的工具函数一起使用特别有用。例如:
class User {
public function getName(): string {
// ...
}
}
function all_names(array<User> $users): string {
$names = array_map($users, meth_caller(User::class, 'getName'));
return implode(', ', $names);
}
当然,有一种值在PHP中是可调用的,但是在Hack的类型检查器中却不能识别:拥有一个__invoke()方法的对象。这个问题未来将会被改进。
泛型
也被称作参数化类型,泛型允许一段代码在同一方式下使用多种不同的类型,并且保证仍然可验证为类型安全的。最简单的例子是取代简单指定一个值是一个数组,你可以通过泛型指定一个字符串组成的数组,或者由一个Person类的对象组成的数组等。
泛型是非常强大的工具,这里仅做简单了解,我们将在第2章做全面的阐述。
对于本章而言,已经足够了解泛型数组的语法了。它通常由关键词array开头,然后紧随着的是一个或者两个由尖括号包住的类型名称。如果在尖括号内只有一个值,那么这个类型就是数组中value的类型,并且数据的key被认为是int类型。如果尖括号内有两个类型,那么第一个就是key的类型,第二个就是value的类型。举例说明如下:array意味着整型数组的key映射到布尔型的值,而array意味着字符串类型的key映射到整型。尖括号里面的类型被称为类型实参。
需要重点说明的是,一个值如果在PHP中不能被创建,那么在Hack中你也不能创建这个值。PHP和Hack的基础元素都是一致的,Hack的类型系统仅仅为更有意思的组合和可能值的子集提供了新的表述方式。
更具体地说,请思考下面的代码:
function main(): void {
f(10, 10);
}
function f(mixed $m, int $i): void {
// ...
}
在函数f()的函数体内,我们说$m是一个mixed类型的变量,而$i是一个int类型的变量,虽然它们实际上存储的是同样的东西。
或者想想下面的代码:
function main(): void {
$callable = function(string $s): ?int { / ... / };
}
虽然我们说变量$callable是(function(string): ?int)类型, 但是在此情况下,它和其他闭包一样,仍然是个对象。它并不是一个仅在Hack中存在的神奇函数指针,或者其他类似的概念。
一般来说,如果我们表述某个变量是某个类型的话,这个表述是类型检查器知晓的,但并不是运行环境所知晓的。