【问题标题】:Hacklang — does this type error involving `this` suggest that the underlying object type matters?Hacklang — 涉及 `this` 的这种类型错误是否表明底层对象类型很重要?
【发布时间】:2017-05-11 10:05:01
【问题描述】:

我认为以下示例是类型安全的:在所有三种情况下,我都试图实例化一个期望 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 subclass hacklang


    【解决方案1】:

    引用documentation:

    this 只能用作类的方法上的返回类型注解。 this 表示该方法返回一个与定义该方法的类相同的对象。

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

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

    作为一般解释,this 始终表示类的当前实例。这并不意味着当前的类类型。在静态上下文中,这有点放宽,表示当前类型,但不是所有子类型。

    这让我们明白了,你的静态代码被破坏了:

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

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

    【讨论】:

    • 感谢第 2 点和第 3 点。这些都是粗心的错误,我已经相应地修改了我的示例。然而,this 类型作为参数类型已经相当完善:this commit 是指示性的。我的问题仍然存在:错误是什么意思?显然,this 的论点并非无处不在。
    【解决方案2】:

    更新

    更完整的答案是允许子类继承 this-argumented 方法从根本上是有缺陷的。从基础开始,函数参数是逆变的,所以我们可以将它们向下强制转换,因为函数可以接受它指定类型的任何子类型,但它不能接受超类型。因此,Derived &lt;: Basefunction(Derived): voidfunction(Base): void 的转换无效。通过扩展,以下层次结构无效,因为Derived &lt;: 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 的主体至少可以调用具有this 参数的$v-&gt;act
    3. act 可以从 A 上下文中调用,因此参数 this 正好是 A。因此,$v 的上下文中的this转换为A
    4. 因为A 不是最终的,所以$v 可能是A 的正确子类型。
    5. $v 的基础类(在本例中为B)可以覆盖来自A 的任何this-argumented 方法,假设this &lt;: B。在这种情况下,$v-&gt;b_prop 依赖于该假设。
    6. A 上下文认为它可以通过 (3.) 将 A 类型(例如其自身)传递给 B,但这违反了 (5.) 中的假设

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-07-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-05
      • 2010-11-27
      • 1970-01-01
      • 2017-07-26
      相关资源
      最近更新 更多