《Hack与HHVM权威指南》——2.7 进阶:协变和逆变

本节书摘来自华章出版社《Hack与HHVM权威指南》一书中的第2章,第2.7节,作者 Owen Yamauchi,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.7 进阶:协变和逆变

除非你正在编写一些很常见的,类似集合(collection)的代码库,否则你可能并不需要阅读下面的内容。对于日常上大多数用例来说,你只需要知晓我们正在讨论的规则是存在的,并且知道为什么。本小节的内容是关于“当你必须要修改相关规则的时候将如何做”。
关于泛型的亚型关系是如何被它们类型实参的亚型关系影响的概念,我们称之为“变体(variance)”。这里三种不同的变体。设想一下,我们有个泛型类叫做Thing,这个类有个类型形参T。然后(我们使用int和num作为类型实参的示例):
如果Thing是Thing的亚型,我们说Thing对于T是协变的(covariant)。数组在它所有的类型形参上都是协变的,不可变集合类在它们值的类型形参上也都是协变的。
如果Thing是Thing的亚型,我们说Thing在T上是逆变的(contravariant)。这违反直觉,但这也是可能的,这里就有逆变类型的真实程序。
如果上面两项都不为真,那么我们说Thing在T上是不变的。

2.7.1 语法

为了使一个泛型在它的类型形参上是协变,就放置一个加号在类型形参之前。请仅在参数列表上做这件事情。在定义内部,还和以前一样使用类型形参的名字。类似地,如果想使一个泛型在它的类型形参上是逆变的,那么请放置一个减号在类型形参之前。如下所示:

class CovariantOnT<+T> {
  private T $value;??// No + here
  // ...
}

class ContravariantOnT<-T> {
  private T $value;??// No - here
  // ...
}
class InvariantOnT<T> {
  private T $value;
  // ...
}

一个类可以有不同的变量类型形参:

class DifferentVariances<Tinvariant, +Tcovariant, -Tcontravariant> {
  // ...
}

下面有一些帮助你记忆相关术语和语法的几个知识点:
协变(Covariance)
单词的前缀“co-”意味着“协同(with)”,对于一个协变类型形参来说,泛型的亚型关系和实参的亚型关系“在同一个方向上”变化,因为它们沿相同的方向变化,所以对应的符号是个加号。
逆变(Contravariance)
单词的前缀“contra-”意味着“对抗(against)”,对于一个逆变类型的形参来说,泛型的亚型关系和实参的亚型关系相反。因为它们沿相反的方向变化,所以符号是个减号。

2.7.2 什么时候使用它们

对于你写下的大多数类来说,并不会用到协变或者逆变。这个特性仅在一些特殊情况下有作用:
协变主要用于只读类型。比如,如果我们从类Wrapper中移除方法setValue(),那么对于它的类型形参Tval来说,它就是只读的。也就是说,它只输出类型Tval的值;除了在构造函数中,它从不使用它们作输入项目。所以在Tval上,Wrapper能够是协变的注4。
逆变是用来只写类型的。举例来说,一个序列化类型T的值到日志文件的泛型类,对于类型T的值可能是只写的。更确切地说,它只把类型T的值为输入,但是从来不输出它们。
类型检查器通过在怎样使用协变和逆变类型形参上设置限制条件来执行它。明确地说,每种类型形参只允许出现在代码上的特定位置,它们被称为协变位置和逆变位置。
首先,我们介绍简单的部分:
public和protected的属性类型仅被限制为不可变类型形参。
返回类型被限制为不可变或者协变的类型形参。它们是协变位置。
除构造函数外,函数和方法的形参类型被限制为不可变或者逆变的类型形参。它们是逆变位置。
私有属性类型和构造函数形参类型没有类型形参方面的限制。
然后,我们说明稍微复杂的部分。在另外一个逆变位置内部,还可以有另外一个逆变位置。而内部逆变位置事实上是协变的。请看下面的示例:

class WriteOnly<-T> {
  private T $value;
  public function __construct(T $value) {
    $this->value = $value;
  }
  // 错误!
  public function passToCallback((function(T): void) $callback): void {
    $callback($this->value);
  }
}

逆变类型形参T出现在一个形参类型($callback的类型)上,而后一个参数类型又出现在了另外一个形参类型(passToCallback()的类型) 的内部。这就是在一个逆变位置内部的另外一个逆变位置。所以它是协变的。因此它是非法的。
你可以具体看看为什么会是这样,直觉上,passToCallback()的写法将导致对于WriteOnly外部的内容,在WriteOnly实例之外出现得到类型T值的可能性。
协变位置内部的协变位置仍然是协变的。协变和逆变的工作就像乘法下的正数和负数,正数乘以正数得到正数,但是负数乘以负数得到的也是正数。
协变
让我们在类Wrapper中移除方法setValue(),然后把类型形参设置为协变的:

class Wrapper<+Tval> {
  private Tval $value;
  public function __construct(Tval $val) {
    $this->value = $val;
  }
  public function getValue(): Tval {
    return $this->value;
  }
}

协变类型形参Tval以如下情形出现:一个私有属性的类型、一个构造函数的参数、一个返回类型。这些都是协变类型形参允许出现的地方。类型检查器将会接受这些代码而不会报错。
接下来的代码现在也是被接受的。当我们把Wrapper按照Wrapper来对待的时候,放置在协变类型形参上的限制条件保证了这里没有什么途径可以打破类型安全限制。

function takes_wrapper_of_num(Wrapper<num> $w): void {
  // ...
}
function takes_wrapper_of_int(Wrapper<int> $w): void {
  takes_wrapper_of_num($w);  // OK
}

如果你试图添加一个改变值的方法,类型检查器将会报告一个错误:一个协变类型形参出现在一个不可协变的位置上。

class Wrapper<+Tval> {
  public function setValue(Tval $value): void {??// Error
    $this->value = $value;
  }

  // ...
}

同样的道理,如果你改变$value属性的访问修饰符为public或者protected,类型检查器也会报告一个错误:一个非私有的属性总是一个不变的位置。也就是说,你不能在这里使用协变或者逆变的类型形参。
逆变
逆变类型是比较少见的,这是因为只写的类型比只读的类型罕见。我们接下来将会通过一个类来加深对逆变的理解。在这个类中,我们建立了一个值的

buffer,然后把JSON以流的形式写入其中。
class JSONLogger<-Tval> {
  private resource $stream;
  private array<Tval> $buffer = array();
  public function __construct(resource $stream) {
    $this->stream = $stream;
  }
  public function log(Tval $value): void {
    $buffer[] = $value;
  }
  public function flush(): void {
    fwrite($this->stream, json_encode($this->buffer));
    $this->buffer = array();
  }
}

请注意,逆变类型形参Tval只出现在一个方法的参数和一个私有属性之中,所以类型检查器接受了这段代码。如果你试图把$buffer设置为public或者protected,或者为这个类添加一个返回类型为Tval的方法,类型检查器都会报告一个错误。
这个违反直觉的逆变类型形参意味着JSONLogger是JSONLogger的一个亚型。由下面的代码来进行验证:

function wants_to_log_ints(JSONLogger<int> $logger): void {
  $logger->log(20);
}
function wants_to_log_nums(JSONLogger<num> $logger): void {
  wants_to_log_ints($logger);?? // OK
  $logger->log(3.14);
}

这里的代码把JSONLogger传递给了一个期待参数为JSONLogger的函数。这是可以的,因为JSONLogger可以做任何JSONLogger可以做的事情(甚至更多)。因为这里在JSONLogger之外,没有任何途径可以获取Tval类型的值。在这个类外部的代码也不能从它获取其不期待的类型的值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值