Hacklang — 这种涉及"this"的类型错误是否表明底层对象类型很重要?



我认为以下示例是类型安全的: 在所有三种情况下,我都在尝试实例化一个期望thisA,但没有太多运气:

<?hh // strict
class A {
public function __construct(?this $next = null) {}
}

// Attempt 1: infer from return type
function foo(): A {
return new A(foo());
}
// Attempt 2: infer from argument type
function bar(A $A): void {
new A($A);
}
class B {
// Attempt 3: infer from property type
public ?A $A;
public function baz(): void {
new A($this->A);
}
}

因为在所有情况下,打字器都抱怨:

[构造函数参数源自的A] 的后期绑定类型需要完全A

由于A不是最终的,因此这可能是子类的实例。

唯一不这样做的情况是在同一范围内实例化A

class B {
public function foo(): void {
new A(new A());
}
}

我猜根本原因是所有失败案例中的基础对象可能是向上转换为A的子实例?我的主要困惑是为什么这会使实例化(或者,一般来说,任何方法调用)不健全。

引用文档:

this只能用作类方法返回类型注释。this表示该方法返回定义该方法的同一类的对象。

返回this的主要目的是允许在类本身或其子类的实例上链接方法调用。

换句话说,完全不支持使用this作为类型提示,将来可能会中断。

至于一般解释,this总是意味着类的当前实例。它并不意味着当前的类类型。在静态上下文中,这稍微放宽了一点,表示当前类型,但不是所有子类型。

这就引出了一点,你的静态代码被破坏了:

  1. 尝试 1:foo() 在无限循环中递归调用自己。
  2. 尝试 2:bar() 尝试访问一个名为A的成员变量,该变量不可为空且未初始化。(在尝试 3 中定义。此外,您不能从静态上下文访问非静态上下文 ($this)。
  3. 尝试
  4. 3:同样,您正在尝试使用名为A的不可为空的未初始化成员变量。你不能在Hack中做到这一点。

如果您希望代码正常工作,则需要理顺OO流程。

已更新

更完整的答案是允许子类继承this参数方法从根本上是有缺陷的。从基础开始,函数参数是逆变的,因此我们可以向下转换它们,因为函数可以接受它指定的类型的任何子类型,但它不能接受超类型。因此,Derived <: Basefunction(Base): void的演员function(Derived): void无效。通过扩展,以下层次结构无效,因为Derived <: Base,但DerivedBase强制转换的转换以相同的方式foo(...)

<?hh // strict
interface Base {
public function foo(Base $v): void;
}
interface Derived extends Base {
<<__Override>>
public function foo(Derived $v): void;
}
// Derived -> Base means foo(Derived): void -> foo(Base): void

现在,请注意,this类型正是这样做的,即使Derived没有显式覆盖foo(...)。换句话说,以下内容是完全等效的:

<?hh // strict
interface Base {
public function foo(this $v): void;
}
interface Derived extends Base {}

我能想到的最简单的违规行为:

<?hh
interface Base {
public function foo(this $v): void;
}
final class Derived implements Base {
public function bar(): void {}
public function foo(this $v): void {
$v->bar(); // trying to call `bar()` on OtherDerived fails miserably!
}
}
final class OtherDerived implements Base {
public function foo(this $v): void {}
}
function violate(Base $v, Base $x): void {
$v->foo($x);
}
bar(new Derived(), new OtherDerived());

但是,如果我们能确定我们有一个特定的Base后代,那么我们确切地知道它的foo(...)想要什么:它自己的类型,而且只有它自己的类型。我们通常可以将其应用于第一个代码块中显示的覆盖。这通常不是很有用,但我认为this是如此漂亮和简洁,值得允许编写接口,然后检查每个调用。


源语言

如果没有该错误,则可能会出现以下冲突:

<?hh // strict
class A {
public function act(this $v): void {
$v->act($this);
}
}
class B extends A {
public ?int $b_prop;
<<__Override>>
public function act(this $v): void {
$v->b_prop;
}
}
function initiate(): void {
violate(new B());
}
function violate(A $v): void {
(new A())->act($v);
}

我不知道这是否是此错误的典型违规行为,但我对基本原理的猜测:

  1. A::act(this)是公共/受保护的,因此通过它的存在,至少存在一个方法,其this参数可用于子类。
  2. act的主体至少可以调用$v->act具有this参数。
  3. 可以从A上下文调用act,因此参数this正好A。由此可见,$v语境中的this也被投射到A
  4. 由于A不是最终的,因此$v可能是A的适当亚型。
  5. $v的基础类(在本例中为B)可以在假设this <: B的情况下覆盖A中的任何this参数方法。在这种情况下,$v->b_prop依赖于该假设。
  6. A上下文认为它可以通过 (3.) 将A类型(例如自身)传递给B,但这违反了 (5.) 中的假设。

相关内容

  • 没有找到相关文章

最新更新