《Hack与HHVM权威指南》——1.4 Hack的类型系统

本节书摘来自华章出版社《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中存在的神奇函数指针,或者其他类似的概念。
一般来说,如果我们表述某个变量是某个类型的话,这个表述是类型检查器知晓的,但并不是运行环境所知晓的。

时间: 2024-09-19 08:15:07

《Hack与HHVM权威指南》——1.4 Hack的类型系统的相关文章

《Hack与HHVM权威指南》——第1章 类型检查

本节书摘来自华章出版社<Hack与HHVM权威指南>一书中的第1章,第1节,作者 Owen Yamauchi,更多章节内容可以访问"华章计算机"公众号查看. 第1章 类型检查 类型检查器是Hack语言的标志特性,它对Hack程序静态地进行分析(不用运行它们),并且能够检查很多种错误.这就能够在程序开发初期尽量避免bug,并且使程序更容易阅读和理解.为了增强类型检查器的能力,Hack语言允许编程人员显式地在程序体中标注某些变量值的类型,比如函数参数.函数返回值和属性值,类型检

《Hack与HHVM权威指南》——1.7.1 提炼nullable类型到non-nullable类型

本节书摘来自华章出版社<Hack与HHVM权威指南>一书中的第1章,第1节,作者 Owen Yamauchi,更多章节内容可以访问"华章计算机"公众号查看. 1.7.1 提炼nullable类型到non-nullable类型 null检查语句在从空值(nullable)的类型到非空值(non-nullable)类型的转变中经常用到.下面是个通过了类型检查器检查的示例. function takes_string(string $str) { // ... } functio

《Hack与HHVM权威指南》——1.5.1 使用超级全局变量

本节书摘来自华章出版社<Hack与HHVM权威指南>一书中的第1章,第1.5.1节,作者 Owen Yamauchi,更多章节内容可以访问"华章计算机"公众号查看. 1.5.1 使用超级全局变量 超级全局变量是在每个代码范围内都存在的全局变量,不需要使用global语句即可使用.这些在运行环境中特殊存在的变量一共有9个,分别是: $GLOBALS $_SERVER $_GET $_POST $_FILES $_COOKIE $_SESSION $_REQUEST $_ENV

《Hack与HHVM权威指南》——1.5.3 属性值初始化

本节书摘来自华章出版社<Hack与HHVM权威指南>一书中的第1章,第1.5.3节,作者 Owen Yamauchi,更多章节内容可以访问"华章计算机"公众号查看. 1.5.3 属性值初始化 为了维护类型安全,类型标注过的属性在初始化时,无论是严格模式还是局部模式,类型检查器会强加一些规则.首要目标就是确保属性值在没有初始化为正确类型的值之前,不能被读取. 对于静态的属性值,规则非常简单:任何不可为空的(non-nullable)的静态属性值都必须有一个初始化值.没有显式初

《Hack与HHVM权威指南》——第1章 类型检查1.7 类型提炼

本节书摘来自华章出版社<Hack与HHVM权威指南>一书中的第1章,第1.7节,作者 Owen Yamauchi,更多章节内容可以访问"华章计算机"公众号查看. 1.7 类型提炼 假设你有个?string类型的值,而且准备把这个值传递给一个参数类型为string的函数.那么你怎么把一个类型(?string)转化为另外一个类型(string)呢?或者假设你有个object类型的值,它可能实现或没有实现Polarizable接口.同时,如果它实现了这个接口,你还希望调用这个ob

《Hack与HHVM权威指南》——第2章 泛型2.1 入门实例

本节书摘来自华章出版社<Hack与HHVM权威指南>一书中的第2章,第2.1节,作者 Owen Yamauchi,更多章节内容可以访问"华章计算机"公众号查看. 第2章 泛型 泛型在Hack的类型系统里面是个非常强大的特性,泛型可以允许你在不知道流程中传入的具体类型的情况下,写出类型安全的代码.一个类或者函数都可以是泛型的,这意味着它可以让调用者来选择传入的参数类型. 泛型结构体最好的例子就是数组和集合类(关于集合类的更多内容请参见第5章).不具备明确指出数组内容具体类型的

《Hack与HHVM权威指南》——1.2.1 自动加载一切

本节书摘来自华章出版社<Hack与HHVM权威指南>一书中的第1章,第1.2.1节,作者 Owen Yamauchi,更多章节内容可以访问"华章计算机"公众号查看. 1.2.1 自动加载一切 类型检查器做出的一个关键假设就是,你的项目经过设置后,代码库中的任何类.函数或者常量都能够在你代码库的其他地方使用.不会尝试去分析任何include或者require语句,确保当前文件在使用时已经加载了其他文件.相反,它认为你已经完成了"自动加载"的相关设置.这就简

《Hack与HHVM权威指南》——1.4.1 类型检查器模式

本节书摘来自华章出版社<Hack与HHVM权威指南>一书中的第1章,第1.4.1节,作者 Owen Yamauchi,更多章节内容可以访问"华章计算机"公众号查看. 1.4.1 类型检查器模式 Hack的类型检查器有三种模式:严格(strict).局部(partial)和耦合(decl).这些模式都基于一个个单独的文件,不同模式下的单独文件可以无缝地进行对接.每个文件可以单独声明它的类型检查模式,语法就是在文件的第一行使用一个双斜线的注释.如下面的代码所示:<?hh

《Hack与HHVM权威指南》——1.5.6 在switch语句中的越界

本节书摘来自华章出版社<Hack与HHVM权威指南>一书中的第1章,第1.5.6节,作者 Owen Yamauchi,更多章节内容可以访问"华章计算机"公众号查看. 1.5.6 在switch语句中的越界 在switch语句中有从一个分支不小心越界到另外一个分支的常见错误.Hack添加了一条规则来捕获这种错误.下面将展示一个案例,从一个case分支越界到另外一个分支将会引发一个错误,除非第一个分支是空的: switch ($day) { case 'sun': echo '