php 即使类方法未在接口/父类中声明,类型提示参数是否仍应允许访问子类方法?

5kgi1eie  于 2022-10-30  发布在  PHP
关注(0)|答案(1)|浏览(110)

我注意到PHP中的类型提示并不像其他面向对象语言那样工作。如果一个类型提示参数作为父类/接口表示一个子类的示例,那么在提供的参数上调用一个方法,就允许调用父类和子类中定义的两个方法。这是预期的行为吗?如果是,为什么?
假设有一个接口A提出了一个方法execute,类B实现了这个接口并定义了一个新的方法done。如果类C有一个方法doSomething,它接受一个类型为A的参数,如果对象是类B的示例,则可以对参数同时调用executedone方法。看起来如下所示:

interface A
{
    public function execute(): void;
}

class B implements A
{
    public function execute(): void
    {
        echo 'inside execute';
    }
    public function done(): void
    {
        echo 'inside done';
    }
}

class C
{
    public function doSomething(A $obj): void
    {
        $obj->execute(); // This works as expected.
        $obj->done();    // This works even though the object is type hinted as A
    }
}
fslejnso

fslejnso1#

The documentation在[***粗斜体***强调我]上不是很清楚:
类型声明可以添加到函数参数、返回值和类属性中(从PHP 7.4.0开始)。它们确保值在调用时是指定的类型***,否则会抛出TypeError。
这一句话是我能找到的PHP类型系统语义的全部文档。
正如您所看到的,这有些模糊:“在呼叫时间”到底是什么意思?
看起来这是这样解释的,
类型检查只对调用中的参数进行 。所以,唯一发生的类型检查是,当函数doSomething被调用时,函数doSomething的参数$objA类型。它是这样的,所以没有错误。
PHP Language Specification也没有多大帮助,关于Types的部分、关于Functions的部分和关于Classes的部分都没有回答这个问题,而the entire spec does not even contain the word "hint" once
关于 * 类型检查模式 * 的小节似乎指出类型检查只对函数调用中的函数参数进行[***bold italic
emphasis mine]:
对于强制模式,如果***传递的值***与***参数***的类型相同,则接受该值。[...]
对于严格模式,***参数***必须与声明的类型完全相同[...]
[...]
请注意,类型检查模式用于由调用方而不是被调用方控制的函数调用。
关于 * 函数定义 * 的小节如下所述:
默认情况下,参数将接受任何类型的参数。但是,通过指定 type-declaration,可以限制接受的参数类型。通过指定array,仅接受array类型的参数。通过指定callable,仅接受指定函数的参数(请参阅下文)。借由指定iterable,只会接受数组型别的参数或实作Traversable界面的对象。借由指定 qualified-name,只会接受具有该型别或衍生自该型别之类别的执行严修,,或者只接受直接或间接实作该界面型别的类别执行严修。
当然,还有 Return Typing,但这与本例无关。
然而,这仅仅是在 * 定义 * 方面,那么 * 调用 * 方面呢?
不,关于Expressions的部分也没有帮助。
有关 * 成员访问运算符 *(->)的小节通常会列出以下约束条件:

  • dereferencable-expression* 必须指定一个对象,或者是NULLFALSE或空字符串。
  • expression* 必须是string类型的值(但不是字串常值),其中包含执行严修属性(不含前置$)或该执行严修之类别类型的执行严修或静态方法的名称。

关于 Member Call Operator 的特定小节提到了类型检查或提示。Member Call Operator 只有这些约束:

  • dereferencable-expression* 必须指定一个对象。

此外,一般函数调用约束也适用。
“常规函数调用约束条件”列在"* 函数调用运算符 *“小节中:

  • callable-expression* 必须指定一个函数,方法是作为包含函数名称的字符串类型的值,或者作为实现__invoke方法的类型的对象(包括Closure对象)。

函数调用中存在的参数数必须至少与为该函数定义的非可选参数数一样多。
在条件定义的函数存在之前,不能对该函数进行调用。
任何与byRef传递的参数匹配的参数都应该(但不必)指定左值。
如果使用了 variadic-unpacking,则表达式的结果必须是数组或Traversable。如果提供了不兼容的值,则忽略该参数并发出非致命错误。
同样,这里根本没有提到类型检查,除了一个显而易见的事实:如果使用了 variadic-unpacking,就一定有要解包的内容。
这是预期行为吗?如果是,为什么?
你的第一个问题的答案似乎是“是的,这是预期的行为”。你的第二个问题只能由编写规范的人来回答,除非你满足于琐碎的答案“因为规范是这样说的”
这种行为无疑是令人吃惊的:你的问题中的代码显然是不安全的,但是它没有被类型系统捕获。2但是这是一个很好的提醒,* 类型检查器只检查类型检查器定义要检查的内容 *。3或者,换句话说:类型检查器是否认为您的程序是 type-safe 取决于类型系统对 type-safe 的定义。
PHP only 认为传递错误类型的参数是不安全的,但不会调用随机方法。
这与其他语言的处理方式不同。例如,在下面的Python代码中,它大致相当于你的示例:

from typing import Protocol

class A(Protocol):
    def execute(self) -> None:
        ...

class B(A):
    def execute(self) -> None:
        print("inside execute")

    def done(self) -> None:
        print("inside done")

def do_something(obj: A) -> None:
    obj.execute()
    obj.done()

MypyPyright这两个Python中使用最广泛的类型检查器都抱怨:

  • Mypy:“A”没有属性“done”
    • 派莱特 *:
  • 无法访问类型“A”的成员“done”

成员“done”未知

  • “完成”类型未知

与此TypeScript示例相同:

interface A {
    execute(): void;
}

class B implements A {
    execute(): void {
        console.log('inside execute');
    }
    done(): void {
        console.log('inside done');
    }
}

function doSomething(obj: A): void {
    obj.execute();
    obj.done(); // Property 'done' does not exist on type 'A'.
}

相关问题