【问题标题】:Can you override interface methods with different, but "compatible", signatures?您可以使用不同但“兼容”的签名覆盖接口方法吗?
【发布时间】:2013-02-07 07:45:19
【问题描述】:

考虑以下 PHP 接口:

interface Item {
    // some methods here
}

interface SuperItem extends Item {
    // some extra methods here, not defined in Item
}

interface Collection {
    public function add(Item $item);
    // more methods here
}

interface SuperCollection extends Collection {
    public function add(SuperItem $item);
    // more methods here that "override" the Collection methods like "add()" does
}

我正在使用 PHPStorm,当我这样做时,我在 IDE 中收到一个错误,该错误基本上表明 SuperCollection 中的 add() 的定义与它扩展的接口中的定义不兼容 Collection .

在某种程度上,我可以看到这是一个问题,因为该方法的签名与它“覆盖”的签名不匹配完全。但是,我确实觉得这是兼容的,因为SuperItem 扩展了Item,所以我认为add(SuperItem) 与add(Item) 相同。

我很好奇 PHP(5.4 或更高版本)是否支持此功能,也许 IDE 存在无法正确捕获此问题的错误。

【问题讨论】:

  • 在 5.3 中,我得到“致命错误:SuperCollection::add() 的声明必须与 Collection::add() 的声明兼容”。所以看起来 PhpStorm 是正确的。为什么您认为 5.4 的行为可能会有所不同?

标签: php interface phpstorm


【解决方案1】:

不,我很确定 PHP 在任何版本中都不支持这一点,它宁愿破坏接口的意义。

接口的意义在于它为您提供了与引用相同接口的其他代码的固定合同。

例如,考虑这样一个函数:

function doSomething(Collection $loopMe) { ..... }

这个函数期望接收一个实现Collection接口的对象。

在函数中,程序员可以编写对Collection 中定义的方法的调用,因为知道对象会实现这些方法。

如果你有一个这样的重写接口,那么你就有问题了,因为SuperCollection 对象可以传递给函数。它会起作用,因为由于继承,它也是一个Collection 对象。但随后函数中的代码无法再确定它知道add()方法的定义是什么。

根据定义,接口是固定合同。它是不可变的。

作为替代方案,您可以考虑使用抽象类而不是接口。这将允许您在非严格模式下进行覆盖,尽管出于同样的原因,如果您使用严格模式仍然会出现错误。

【讨论】:

  • 感谢您的解释!我在想这应该在我的脑海中起作用,但你帮我看到了其中的错误。
  • 我不同意这个答案。接口不关心方法定义或其中的关系(例如,add()ing 一个项目意味着您也可以delete() 它)。他们唯一的职责是执行兼容的方法签名(输入/输出)。您对它们是固定合同是正确的,但对该合同的目的不正确。两个类可以很容易地实现相同的接口并且具有完全不同的语义(例如add()实际上可以删除一个类中的项目,只要输入/输出保持相同类型)。
  • @drrcknlsn 虽然我不认为它会使解释无效,但要牢记这一点。
  • @FtDRbwLXw6 这是一个解释问题,但我认为接口有责任扩展到函数的语义,而不仅仅是签名。许多著名的 PHP 接口都带有文档块,例如github.com/php-fig/log/blob/master/Psr/Log/LoggerInterface.php 或 github.com/php-fig/http-message/blob/master/src/… 。实施者需要阅读文档以确保它们不违反 Liskov 替换原则。这是 PHP 具有名义而非结构类型的优势。
【解决方案2】:

作为一种解决方法,我在接口中使用 PHPDoc 块。

interface Collection {
   /**
    * @param Item $item
    */
    public function add($item);
    // more methods here
}

interface SuperCollection extends Collection {
    /**
    * @param SuperItem $item
    */
    public function add($item);
    // more methods here that "override" the Collection methods like "add()" does
}

这样,如果您正确使用接口,IDE 应该可以帮助您捕获一些错误。您也可以使用similar technique 来覆盖返回值类型。

【讨论】:

    【解决方案3】:

    2014 年 11 月发布的 PHP 7.4 有 improved type variance。

    问题中的代码仍然无效:

    interface Item {
        // some methods here
    }
    
    interface SuperItem extends Item {
        // some extra methods here, not defined in Item
    }
    
    interface Collection {
        public function add(Item $item);
        // more methods here
    }
    
    interface SuperCollection extends Collection {
        public function add(SuperItem $item); // This will still be a compile error
        // more methods here that "override" the Collection methods like "add()" does
    }
    

    因为Collection 接口保证任何实现它的东西都可以接受Item 类型的任何对象作为add 的参数。

    但是,以下代码在 PHP 7.4 中有效:

    interface Item {
        // some methods here
    }
    
    interface SuperItem extends Item {
        // some extra methods here, not defined in Item
    }
    
    interface Collection {
        public function add(SuperItem $item);
        // more methods here
    }
    
    interface SuperCollection extends Collection {
        public function add(Item $item); // no problem
        // more methods here that "override" the Collection methods like "add()" does
    }
    

    在这种情况下,Collection 保证它可以接受任何SuperItem。由于所有SuperItems 都是Items,SuperCollection 也做了这个保证,同时也保证它可以接受任何其他类型的Item。这被称为Contravariant method parameter type。

    在早期版本的 PHP 中存在有限形式的类型变化。假设其他接口与问题中一样,SuperCollection 可以定义为:

    interface SuperCollection extends Collection {
        public function add($item); // no problem
        // more methods here that "override" the Collection methods like "add()" does
    }
    

    这可以解释为任何值都可以传递给add 方法。这当然包括所有Items,所以这仍然是类型安全的,或者它可以解释为意味着可以传递一个未指定的值类,通常记录为mixed,程序员需要使用其他确切的知识什么可以使用该功能。

    【讨论】:

      【解决方案4】:

      【讨论】:

      • 为新参数分配默认值的情况除外。
      【解决方案5】:

      扩展接口不允许改变方法定义。如果您的 SuperItem 正在扩展 Item,它应该毫无问题地通过实现 Collection 接口的类。

      但根据你真正想做的,你可以试试:

      • 为 SuperItem 使用稍微不同的方法创建接口并实现它:

        interface SuperCollection extends Collection {
            public function addSuper(SuperItem $superItem);
        }
        
      • 使用装饰器模式创建几乎相同的界面而无需扩展:

        interface Collection {
            public function add(Item $item);
            // more methods here
        }
        
        interface SuperCollection {
            public function add(SuperItem $item);
            // more methods here that "override" the Collection methods like "add()" does
        }
        

        然后是装饰器(抽象)类,它将使用这个接口:

        class BasicCollection implements Collection {
            public function add(Item $item)
            {
            }
        }
        
        class DecoratingBasicCollection implements SuperCollection {
            protected $collection;
        
            public function __construct(Collection $collection)
            {
                $this->collection = $collection;
            }
        
            public function add(SuperItem $item)
            {
                $this->collection->add($item);
            }
        }
        

      【讨论】:

        【解决方案6】:

        问题不在 IDE 中。在 PHP 中,您不能覆盖方法。并且兼容性只是相反的方向 - 您可以安全地期待父类的实例并接收子类。但是当你期望一个子类时,如果你接收到父类,你就不会安全——子类可能定义了父类中不存在的方法。但是,您仍然不能覆盖该方法

        【讨论】:

        • 拜托,请尝试使用正确的词。你可以在 PHP 中覆盖一个方法,只是不要重载它。
        【解决方案7】:

        当我有一个可能需要重载的方法(PHP 不支持该方法)时,我确保方法参数之一(通常是最后一个)是一个数组。通过这种方式,我可以通过任何我需要的东西。然后我可以在函数中测试各种数组元素,告诉我需要在方法中执行什么例程,通常是在一个选择/案例中。

        【讨论】:

          猜你喜欢
          • 2017-02-16
          • 1970-01-01
          • 2020-07-06
          • 1970-01-01
          • 2017-10-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-11-02
          相关资源
          最近更新 更多