Hacklang - 为什么受保护成员不变?

时间:2016-07-19 23:14:07

标签: casting covariance hacklang

公共成员案例

通过全权访问,调用范围对他们来说,公共成员是不变的并不奇怪:

<?hh // strict
class Foo<+T> {
  public function __construct(
    public T $v
  ) {}
}
class ViolateType {
  public static function violate(Foo<int> $foo): void {
    self::cast_and_set($foo);
    echo $foo->v + 1; // string + integer
  }
  public static function cast_and_set(Foo<arraykey> $foo): void {
    $foo->v = "Poof! The integer `violate()` expects is now a string.";
  }
}
// call ViolateType::foo(new Foo(1)); and watch the fireworks

这里的问题是 {/ strong> violatecast_and_set可以读取和修改具有不同类型预期的相同值(Foo->v)。

但是,对于受保护的成员来说,这个问题似乎并不存在。

尝试为受保护的成员创建违规行为

由于privateprotected之间的唯一区别是对后代的可见性,因此我们需要一个类(ImplCov),在一些受保护的成员之外,否则对类型有效协变,并将其扩展为该类型的类(ImplInv)不变量。值得注意的是,在T上保持不变可以让我公开一个公共二传手 - violate(T $v): T - 我试图打破类型。

<?hh // strict
// helper class hierarchy
class Base {}
class Derived extends Base {}

class ImplCov<+T> {
  public function __construct(
    protected T $v
  ) {}
}
class ImplInv<T> extends ImplCov<T> {
  public function violate(T $v): T {
    // Try to break types here
  }
}

使用ImplInv<Derived>的实例,我被强制转换为ImplCov<Derived>,然后利用协方差转换为ImplCov<Base>。这是最危险的事情,所有三种类型都指同一个对象。让我们检查每种类型之间的关系:

  1. ImplInv<Derived>ImplCov<Base>:当属性更改为超类型(int-&gt; arraykey)或具有公共超类型的不相交类型时,发生公共成员案例中的违规(int - &GT;字符串)。但是,由于ImplCov<Base>T上是协变的,因此不存在可以传递Base实例并使v成为真Base的方法。 ImplCov的方法也无法产生new Base()并将其分配给v,因为它不知道T的最终类型。 1

    同时,由于投射ImplCov<Derived> --> ImplCov<Base> --> ...只会导致其得分较少,因此ImplInv::violate(T)保证在最差情况下将v设置为最终ImplCov&#的子类型39; s T,保证对最终T进行有效投射。 ImplInv<Derived>的派生无法投射,因此一旦参数化,该类型就会被设置。

  2. ImplInv<Derived>ImplCov<Derived>:这些可以共存T在它们之间是相同的,铸造只是最外层的类型。
  3. ImplCov<Derived>ImplCov<Base>:这些可以通过ImplCov有效协变的假设共存。 protected的{​​{1}}可见度与v无法区分,因为它们属于同一类。
  4. 所有这些似乎都表明受保护的可见性对协变类型来说是犹太的。我错过了什么吗?

    1。我们实际上可以通过引入private约束new Base()来生成super,但这甚至更弱,因为根据定义ImplCov<T super Base>必须在ImplInv中参数化ImplCov带有超类型的{1}}语句,使extends的操作安全ImplInv。另外,v无法假设有关ImplCov成员的任何内容。

1 个答案:

答案 0 :(得分:1)

请记住,子类可以修改该类的任何对象的protected成员,而不仅仅是$this。因此,对上面示例的这种轻微修改使用protected成员来类似地破坏类型系统 - 我们所要做的就是使ViolateType成为Foo的子类(并且它不会关于我们设置T的内容,或者我们将ViolateType设置为通用或其他内容。

<?hh // strict

class Foo<+T> {
    public function __construct(
        /* HH_IGNORE_ERROR[4120] */
        protected T $v
    ) {}
}

class ViolateType extends Foo<void> {
    public static function violate(Foo<int> $foo): void {
        self::cast_and_set($foo);
        echo $foo->v + 1; // string + integer
    }

    public static function cast_and_set(Foo<arraykey> $foo): void {
        $foo->v = "Poof! The integer `violate()` expects is now a string.";
    }
}

这使得typechecker只对受保护成员进行了一次错误抑制 - 因此允许受保护成员进行协变会破坏类型系统。