【问题标题】:What is wrong with this example of class inheritance? [duplicate]这个类继承的例子有什么问题? [复制]
【发布时间】:2019-09-25 22:37:15
【问题描述】:

这个依赖倒置的代码应该可以正常工作,但它会报错。

我在这里做错了什么?

interface A  { }

abstract class B implements A { }

class C extends B { }

abstract class D {
    public function foo(A $a) { }
}

class E extends D {
    public function foo(C $c) { }
}

错误是:

警告:E::foo(C $c) 的声明应该与[...][...]中的D::foo(A $a) 兼容24 行

令人惊讶的是,对构造函数方法执行相同的操作就可以了:

interface A  { }

abstract class B implements A { }

class C extends B { }

abstract class D {
    public function __construct(A $a) { }
    public function foo(A $a) { }
}

class E extends D {
    public function __construct(C $c) { }
    public function foo(A $c) { }
}

【问题讨论】:

  • 我在这里测试了 7.3.5 sandbox.onlinephpfunctions.com/code/…
  • 不完全。在这种情况下,参数是逆变的。抽象类 D 接受所有高于 A 的子类型。B 是 A 的子类型,C 是 B 的子类型,B 是 A 的子类型,因此在这种情况下,A 是最后一个特定类型,因此它必须接受高于它的所有内容。
  • 那么为什么在构造函数中一切正常? sandbox.onlinephpfunctions.com/code/…

标签: php oop php-7.3


【解决方案1】:

这里有两个问题。

为什么第一部分会引发WARNING:错误非常明确。任何时候重写方法都需要尊重父级的签名,从而满足客户端应用程序的期望。

class Foo {}

class Bar extends Foo {}

class A {
   public function test(Foo $foo) {}
}

class B extends A {
   public function test(Bar $bar) {}
}

现在,由于B 扩展了A,并且A::test() 期望Foo,用户可能会尝试这样做:

$a = new A();
$b = new B();

$foo = new Foo();
$bar = new Bar();

$b->test($bar);
$b->test($foo);

test() 的第一次调用会起作用,但第二次会失败(并且是致命的):Foo 不是Bar

参数类型比原来的限制性更强,因此打破了客户端应用程序的期望。请注意,如果我们有合法声明 (B::test(Foo)),我们仍然可以调用 $b->test($bar) 并且会按预期工作,因此实际上不需要更改签名。

2019 年 11 月,PHP 7.4 发布时,contravariance will be supported for parameter types, and covariance will be supported for return types;所以你的例子仍然是无效的。

最后,请注意引发的错误是WARNING,它不会立即使程序崩溃。但它警告您,您正在将应用程序打开到可能崩溃的行为,这就是它通常的糟糕设计(出于上述原因)。


第二个问题:为什么构造函数没有得到同样的警告?他们并非有意发出此警告。如果你查看docs,你会发现:

与其他方法不同,当使用与父 __construct() 方法不同的参数覆盖 __construct() 时,PHP 不会生成 E_STRICT 级别的错误消息。

为什么构造函数被区别对待?因为一般理解构造函数不是对象的“公共API”的一部分,不需要受到同样的约束。 Liskov Substitution Principle 处理对象,而不是类;在一个类被实例化之前,还没有对象。因此,构造函数超出范围。

例如,您可以阅读有关此here 的更多信息。

【讨论】:

  • 您的论点解释说“用户可能想尝试做错事”,这完全可以破坏代码并引发错误。但是该声明还没有实现,也没有编写调用该方法的代码。这些方法是否意识到用户将来可能尝试做的错误事情?只要 C 实现了 A,它就应该有合同要求的方法,所以 D 不用担心 C 没有需要的方法,因为事情就是这样工作的:C 必须有所有的方法A. 这不正确吗?
  • 不,不正确。如果 C 覆盖 A 上的方法,则该方法需要兼容。更严格的声明不兼容。
  • Luckyboy,因为它在 SO 中似乎是新的:如果它解决了问题中提出的问题,习惯上“接受”一个答案。我认为我的回答解决了您问题中的每一点。 (另一个答案也很好)。如果您愿意,可以考虑接受答案。
  • 在没有进一步说明的情况下,并非每个答案都是可以接受的。所以我想请你澄清一下为什么 C 可以覆盖接口,只要它的合同根据定义是不可变的。这将是我最后一个问题。
  • 很抱歉,您的问题不清楚。 C 不能“覆盖”接口。它可以实现一个接口。它可以覆盖方法实现。只有当签名与被覆盖方法的签名兼容时,它才能覆盖方法签名。
【解决方案2】:

TL;DR C 正在实现 A,但它也可以从 B 继承其他方法,而实现 A 并不能保证。这造成了两个相同继承类型的类可以有两个具有不同签名的方法的情况。 子类必须接受父类可以接受的所有输入,但如果需要,它可以接受额外的输入,

让我们扩展您的示例。

没什么疯狂的,只是一些示例方法。

interface A
{
    public function gotA();
}

abstract class B implements A
{
    abstract public function gotB();
}

class C extends B
{
    public function gotA()
    {
        // ...
    }

    public function gotB()
    {
        // ...
    }
}

我同意C 是间接实现A,但它也包含来自B 的方法,因此E::foo() 中的调用会非常好。

abstract class D
{
    public function foo(A $a)
    {
        $a->gotA();
    }
}

class E extends D
{
    public function foo(C $c)
    {
        $c->gotB();
    }
}

另一个实现A的示例类。

class ExampleA implements A
{
    public function gotA()
    {
        // ...
    }
}

现在是有趣的部分。在代码中的任何其他位置假定此函数。它假定D(期望A in foo)作为参数。

但是E 也是instanceof D 并且可以传递给x,但它期望Cfoo 中与ExampleA 无关。

function x(D $d)
{
    $exampleA = new ExampleA();
    $d->foo($exampleA);
}


$e = new E();
x($e);

综上所述,x 函数的约定已经满足,但函数损坏了。

你最终得到了两种“不同的”D 类型。一个期待A,一个期待C

至于为什么它不向__constructor 发出警告我不知道。需要进一步调查。

【讨论】:

  • 只要你不能解释构造函数,你的解释就会失败,因为我们有一个不同意理论的相反论点。就像在任何科学中一样。
  • 你的解释是关于为什么代码在错误使用时会失败(当你在 E 中注入 ExampleA 时),而不是为什么声明首先是错误的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-10-05
  • 1970-01-01
  • 2020-04-07
  • 2013-10-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多