为什么PHP 5.2+禁止抽象静态类方法?

122
在启用PHP 5.2中的严格警告后,我看到了一个项目中的许多严格标准警告,而该项目最初是没有使用严格警告编写的:

Strict Standards: 静态函数Program::getSelectSQL() 不应该是抽象的在Program.class.inc中

问题中的函数属于抽象父类Program,并且被声明为抽象静态,因为它应该在其子类(如TVProgram)中实现。
我在这里找到了有关此更改的参考

删除了抽象静态类函数。由于疏忽,PHP 5.0.x和5.1.x允许抽象静态函数存在于类中。从PHP 5.2.x开始,只有接口可以拥有它们。

我的问题是:有人能用清晰明了的方式解释为什么在PHP中不应该有抽象静态函数吗?

14
新读者应注意,PHP 7已经取消了这个不合理的限制。 - Mark Amery
抽象静态方法通知已移除,不会触发错误。 - legend80s
8个回答

80

这是一个悲伤而冗长的故事。

当PHP 5.2首次引入此警告时,静态绑定延迟还未在语言中出现。如果您对静态绑定延迟不熟悉,请注意这样的代码并不能按照您的预期工作:

<?php

abstract class ParentClass {
    static function foo() {
        echo "I'm gonna do bar()";
        self::bar();
    }

    abstract static function bar();
}

class ChildClass extends ParentClass {
    static function bar() {
        echo "Hello, World!";
    }
}

ChildClass::foo();

除了严格模式警告之外,上面的代码不起作用。在foo()中的self::bar()调用明确地引用了ParentClass的bar()方法,即使foo()作为ChildClass的方法被调用也是如此。如果你关闭严格模式并尝试运行这段代码,你会看到"PHP致命错误:无法调用抽象方法ParentClass::bar()"。
鉴于此,在PHP 5.2中抽象静态方法是无用的。使用抽象方法的整个重点是可以编写调用该方法的代码,而不知道它将调用哪个实现 - 然后在不同的子类上提供不同的实现。但由于PHP 5.2没有清洁的方式编写父类的方法来调用所调用的子类的静态方法,因此无法使用抽象静态方法。因此,在PHP 5.2中使用abstract static是糟糕的代码,可能是受到了对self关键字工作原理的误解的启发。对此发出警告是完全合理的。
但是 PHP 5.3 推出了一个新功能,可以通过 static 关键字引用调用方法的类(与 self 关键字不同,后者总是指在该方法中定义的类)。如果您在上面的示例中将 self::bar() 更改为 static::bar(),则在 PHP 5.3 及以上版本中可以正常工作。您可以在 New self vs. new static 中了解更多关于 self vs static 的内容。
有了添加的 static 关键字,abstract static 抛出警告的明显理由消失了。延迟静态绑定的主要目的是允许在父类中定义的方法调用在子类中定义的静态方法;鉴于延迟静态绑定的存在,允许抽象静态方法似乎是合理和一致的选择。
您仍然可以为保留警告辩护。例如,您可以认为由于PHP允许您调用抽象类的静态方法,在我的示例中(即使通过替换selfstatic进行了修复),您正在公开一个ParentClass :: foo()的公共方法,它是错误的,而您不真正希望公开它。使用非静态类 - 即将所有方法作为实例方法,并使ParentClass的子代都成为单例或其他内容 - 可以解决此问题,因为抽象类ParentClass无法被实例化,因此无法调用其实例方法。我认为这个论点很弱(因为我认为暴露ParentClass :: foo()并不是一个大问题,而使用单例代替静态类通常是不必要的冗长和丑陋的),但您可能会有不同的看法 - 这是一个主观性较强的判断。

因此,基于这个论点,PHP开发人员在语言中保留了警告,对吗?

嗯,不完全是这样

与PHP相关的bug报告53081建议删除警告,因为static::foo()构造的添加使得抽象静态方法变得合理和有用。PHP的创建者Rasmus Lerdorf将该请求标记为虚假,并通过一系列错误的推理试图证明该警告的合理性。最后,发生了如下交换:

Giorgio

i know, but:

abstract class cA
{
      //static function A(){self::B();} error, undefined method
      static function A(){static::B();} // good
      abstract static function B();
}

class cB extends cA
{
    static function B(){echo "ok";}
}

cB::A();

Rasmus

Right, that is exactly how it should work.

Giorgio

but it is not allowed :(

Rasmus

What's not allowed?

abstract class cA {
      static function A(){static::B();}
      abstract static function B();
}

class cB extends cA {
    static function B(){echo "ok";}
}

cB::A();

This works fine. You obviously can't call self::B(), but static::B() is fine.

Rasmus声称他的例子中代码“运行良好”是错误的;如您所知,它会抛出严格模式警告。我猜他在没有开启严格模式的情况下进行测试。不管怎样,困惑的Rasmus错误地将请求关闭为“虚假”。因此,这就是为什么警告仍然存在于语言中的原因。这可能不是完全令人满意的解释 - 您可能来到这里希望有一个合理的警告正当化的解释。不幸的是,在现实世界中,有时选择是源于平凡的错误和错误的推理而不是理性的决策制定。这只是其中之一。幸运的是,可敬的Nikita Popov已经在PHP 7中将警告从语言中移除,作为PHP RFC:重新分类E_STRICT通知的一部分。最终,理智获胜,一旦发布了PHP 7,我们就可以愉快地使用abstract static而不会收到这个愚蠢的警告。

77
静态方法属于声明它们的类。当扩展该类时,您可以创建一个同名的静态方法,但实际上并没有实现静态抽象方法。
同样适用于具有静态方法的任何类的扩展。如果您扩展该类并创建具有相同签名的静态方法,则实际上并没有覆盖超类的静态方法。
编辑(2009年9月16日): 就此更新。在运行PHP 5.3时,我看到抽象静态已经回来了,好坏参半。(更多信息请参见http://php.net/lsb
更正(由philfreo进行): 在PHP 5.3中仍然不允许使用abstract staticLSB相关但不同。

3
好的,如果我想要强制所有继承我的抽象类的子类都需要使用函数getSelectSQL(),该怎么办呢?在父类中使用getSelectSQL()没有任何有效的理由存在。最好的行动计划是什么?我选择使用抽象静态方法的原因是,在我已经在所有子类中实现了getSelectSQL()之前,代码就无法编译。 - Artem Russakovskii
1
很可能,你应该重新设计一下,让 getSelectSQL() 成为一个抽象/实例方法。这样,每个子类的实例都会有这样一个方法。 - Matthew Flaschen
7
在 PHP 5.3 中仍然不允许使用抽象静态方法(Abstract static)。这与后期静态绑定(Late static bindings)无关。详情请参阅 https://dev59.com/z3E85IYBdhLWcg3wRBRU。 - Artefacto
41
在我看来,这个严格的警告非常愚蠢,因为PHP具有“延迟静态绑定”,它自然地提供了使用静态方法的想法,就像类本身像Ruby中的对象一样(例如)。这导致静态方法重载,而abstract static在这种情况下可能是有用的。 - dmitry
4
这个答案有点含糊。"still not allowed" 只是意味着你会得到一个 E_STRICT 级别的警告,至少在 5.3+ 版本中,你可以完全自由地创建抽象静态函数、在扩展类中实现它们,然后通过 static:: 关键字引用它们。显然,父类的静态版本仍然存在,不能直接调用(通过 self:: 或 static:: 在该类内部),因为它是抽象的,并会像调用常规非静态抽象函数一样致命错误。从功能上讲,这很有用,我同意 @dmitry 的看法。 - Andy Hoffner
显示剩余6条评论

71

针对这个问题,有一个非常简单的解决方法,从设计角度来看也很合理。正如乔纳森所写:

同样适用于扩展任何带有静态方法的类。如果你扩展该类并创建一个具有相同签名的静态方法,则实际上并没有覆盖超类的静态方法

因此,作为一种解决方法,您可以这样做:

<?php
abstract class MyFoo implements iMyFoo {

    public static final function factory($type, $someData) {
        // don't forget checking and do whatever else you would
        // like to do inside a factory method
        $class = get_called_class()."_".$type;
        $inst = $class::getInstance($someData);
        return $inst;
    }
}


interface iMyFoo {
    static function factory($type, $someData);
    static function getInstance();
    function getSomeData();
}
?>

现在,您需要强制任何继承MyFoo的类实现一个getInstance静态方法和一个public getSomeData方法。如果您不想继承MyFoo,则仍然可以实现iMyFoo来创建具有相似功能的类。


2
使用这种模式,能否将函数设置为protected。当我这样做时,继承MyFoo的类会抛出警告,要求getInstance必须是public。而且你不能在接口定义中使用protected。 - artfulrobot
3
有时候 static:: 很有用。 - seyed
3
无法与Traits一起使用。如果Traits能够拥有abstract static方法,而不是PHP抱怨就好了... - Rudie
1
在这里使用接口可能是最好的解决方案。+1。 - Juan Carlos Coto
这实际上非常优雅,因为它本身的简单性。+1 - G. Stewart
显示剩余2条评论

12
我知道这篇文章是旧的,但是......为什么不直接在父类的静态方法中抛出异常呢?这样如果你没有覆盖它,就会引发异常。

1
那并没有帮助,异常会在调用静态方法时发生 - 如果您不覆盖它,同时会出现“方法不存在”的错误。 - B T
4
我的意思是,不要声明该方法为抽象方法,而是实现它,但在调用该方法时只需抛出异常,这意味着如果已被覆盖,则不会抛出异常。 - Petah
这似乎是最优雅的解决方案。 - Alex S
最好在编译时看到类似这样的东西,而不是运行时。因为你只需要加载文件而不执行代码就能找出问题,所以更容易在生产之前发现问题,而不是在生产中。 - Rahly

4

我认为,抽象类/接口可以被看作程序员之间的合同。它更多地涉及事物应该看起来或行为如何,而不是实现实际功能。正如在php5.0和5.1.x中所看到的,它不是阻止php开发人员这样做的自然法则,而是与其他语言中的面向对象设计模式保持一致的冲动。基本上,这些想法试图防止意外行为,如果已经熟悉其他语言。


虽然这与PHP无关,但这里有另一个很好的解释:https://dev59.com/c3VD5IYBdhLWcg3wXaYd - merkuro
3
我的天啊!给你点踩的这两个人简直疯了!这是本帖中最有见地的回答。 - Theodore R. Smith
6
这段话有错误且不太连贯。其中关于抽象类不实现实际功能的说法并不一定正确,这也是它们存在的全部意义之一,与接口不同。在提到“php开发人员无法做到”的说法中,我不知道“它”指的是什么。而在提到“基本上这些想法试图防止意外行为”的说法中,我不知道“这些想法”或潜在的“意外行为”是什么。无论你从这里获得了什么见解,对我来说都没有意义。 - Mark Amery

2
我不认为禁止静态抽象函数有任何理由。最好的论据是,Java允许使用它们,这表明没有理由禁止它们。 问题是: - 技术上可行吗? - 是的,因为它们在PHP 5.2中存在并且它们也存在于Java中。所以我们可以做到。 - 它们有意义吗?有。在一个类的一部分实现并将另一部分留给用户是有意义的。对于非静态函数来说是有意义的,那么为什么对于静态函数来说就没有意义呢?静态函数的一个用途是必须不存在多个实例的类(单例)。例如加密引擎。它不需要存在多个实例,并且有理由防止这种情况-例如,您只需保护内存的一部分免受入侵者的攻击。因此,实现引擎的一部分并将加密算法留给用户是完全有意义的。 这只是一个例子。如果您习惯使用静态函数,您会发现很多更多的例子。

3
关于你提到的5.2版本中存在抽象静态方法这一点是极具误导性的。首先,严格模式警告禁止使用它们是在5.2版本中引入的;你的说法让人误以为在5.2中它们是被允许的。其次,在5.2版本中,由于晚绑定技术还未出现,不能轻易地使用它们“实现类的某一部分并将另一部分留给用户”。 - Mark Amery

0

在 PHP 5.4+ 中使用 trait:

trait StaticExample {
    public static function instance () {
    return new self;
    }
}

并且在你的类中,在开头加上:

use StaticExample;

1
我成功地将 abstract public static function get_table_name(); 放入一个 trait 中,并在我的抽象类中使用该 trait,不再出现 E_STRICT 警告!这仍然强制要求在子类中定义静态方法,正如我所希望的那样。太棒了! - Programster

-1

了解PHP的“后期静态绑定”问题。如果您把静态方法放在抽象类上,您很可能会很快遇到它。严格的警告提示您避免使用损坏的语言特性是有道理的。


5
我认为他指的是“Strict Standards”警告。 - Jacob Hume
2
你声称晚期静态绑定存在哪些问题?你断言它们是“有问题的”,这是一个大胆的说法,而且没有提供任何证据或解释。这个特性对我来说一直都很好用,我认为这篇帖子是无意义的。 - Mark Amery

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接