重载父方法时,为什么 PHP8.1 转向弃用不兼容的 return 类型?

When overloading a parent method, why did PHP8.1 change towards deprecating incompatible return types?

我像这样重载了 mysqli class 的 query 方法:

class MySql extends \mysqli
{
    function query(string $sql): ?MySqlResult  // line #30
    {
        $result = parent::query($sql);
        return new MySqlResult($result);
    }
}

in PHP8.0 这不是问题。 但是,从 PHP8.1 开始,我现在收到此错误:

Deprecated: Return type of Repository\MySql\MySql::query($sql, $resultmode = null) should either be compatible with mysqli::query(string $query, int $result_mode = MYSQLI_STORE_RESULT): mysqli_result|bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in repository\src\MySql\MySql.php on line 30

我知道如何修复错误 - 我可能最终会更改方法的名称,因为我想 return 我自己的自定义对象。

问题

我正在寻找一个答案,从理论和面向对象的角度捕捉这种变化的需要,可能使用语言理论,或者将其与其他语言进行比较。

为什么需要进行此更改? 进行此更改的必要性或原因是什么? 有什么方法可以在扩展 class 时在 PHP 中允许重载 return 类型?

为了使语言保持一致,必须进行更改。如果重写的方法可以 return 不同的类型,它会导致代码非常混乱。它基本上意味着被覆盖的函数做了完全不同的事情。 This behaviour was never allowed in PHP. 这样的代码总是会抛出:

class A {
    public function foo():string {
        return '';
    }
}

class B extends A {
    public function foo():int {
        return 1;
    }
}

唯一的问题是标准 类 没有在内部指定 return 类型。由于 returning 资源、混合、联合类型等原因,许多方法无法指定类型。这意味着实际上它们没有 return 类型。 PHP 规则说如果重写的方法没有 return 类型,子方法可以指定(缩小)类型:

class A {
    public function foo() { // this could also be :mixed but that was only introduced in PHP 8
        return '';
    }
}

class B extends A {
    public function foo():int {
        return 1;
    }
}

所以,你问错了问题。问题不是为什么自 PHP 8.1 以来不能覆盖 return 类型,因为情况总是如此,而是为什么 PHP internal 类 没有指定return类型。

从 PHP 8.1 开始,declare most return types 成为可能。但是,由于这会导致重大变化,与通常会产生的致命错误相比,内部方法暂时只会抛出 Deprecation 消息。在 PHP 9.0 中,所有这些都将得到修复。


对于您的特定情况,您应该使用组合而不是继承。大多数时候应该避免继承,尤其是内部 类。组合提供了更大的灵活性并且更容易测试。

理解这一点的方法是将函数签名视为 合同。在 built-in mysqli class 中,我们有以下签名:

public function query(string $query, int $result_mode = MYSQLI_STORE_RESULT): mysqli_result|bool

我们可以将其翻译成英文:

  • 如果我有一个 mysqli 的实例 ...
  • ...我可以调用方法query (public) ...
  • ...第一个参数是string...
  • ... 和可选的 int 作为第二个参数 ...
  • ... 我将返回 mysqli_result 对象或布尔值

所以,下面的代码被合约保证运行成功

assert($foo instanceof \mysqli);
$result = $foo->query('Select 1', MYSQLI_USE_RESULT);
assert($result instanceof mysqli_result || is_bool($result));

现在让我们 运行 使用您建议的 class:

实例的代码
assert($foo instanceof \mysqli);
// Success: `MySql` is a sub-type of `\mysqli`
$result = $foo->query('Select 1', MYSQLI_USE_RESULT);
// Success, but second argument ignored
assert($result instanceof mysqli_result || is_bool($result));
// Failure! Function may return null, which doesn't meet this assertion
// If the custom MysqlResult doesn't extend mysqli_result, that will also fail

因此,如您所见,您的 class 未能满足 built-in class 的 合同

从逻辑上讲,这一直是错误,因为您“在精神上”违反了合同,但最近才成为可能PHP到强制执行这个。这就是为什么它目前不是硬错误,这样您就有机会修复“侥幸逃脱”的旧代码。