ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 简介 在使用PHP代码时,您可能经常会遇到`parent::`、`static::`和`self::`。但是当你第一次作为一个开发人员开始的时候,有时候你会很困惑,不知道它们是做什么的,以及它们之间的区别。 在我第一次作为开发人员开始工作后的很长一段时间里,我认为`static::`和`self::`是完全一样的。 ## `parent::`是什么? 假设我们有一个`BaseTestCase`类,它有一个`setUp`方法: ```php class BaseTestCase { public function setUp(): void { echo 'Run base test case set up here...'; } } (new BaseTestCase())->setUp(); // Output is: "Run base test case set up here...'; ``` 正如我们所看到的,当我们调用 setUp 方法时,它按预期运行并输出文本。 现在,让我们假设我们想要创建一个新的`FeatureTest`类来继承`BaseTestCase`类。如果我们想运行`FeatureTest`类的`setUp`方法,我们可以这样做: ``` class FeatureTest extends BaseTestCase { // } (new FeatureTest())->setUp(); // Output is: "Run base test case set up here..."; ``` 正如我们所看到的,我们没有在`FeatureTest`中定义`setUp`方法,所以在`BaseTestCase`中定义的方法将被运行。 现在,假设我们想在运行`FeatureTest`中的`setUp`方法时运行一些额外的逻辑。例如,如果这些类是作为PhpUnit测试的一部分使用的测试用例,那么我们可能需要在数据库中创建模型或设置测试值。 一开始,你可能(错误地)认为你可以在你的`FeatureTest`方法中定义`setUp`方法,然后调用`$this->setUp()`。老实说,当我第一次学习编程的时候,我总是陷入这个陷阱! 所以我们的代码可能看起来像这样: ```php class FeatureTest extends BaseTestCase { public function setUp(): void { $this->setUp(); echo 'Run extra feature test set up here...'; } } (new FeatureTest())->setUp(); ``` 但是,您会发现,如果我们运行这段代码,我们最终会陷入一个循环,导致您的应用程序崩溃。这是因为我们递归地要求`setUp`一遍又一遍地调用它自己。你可能会得到类似这样的输出: ``` Fatal error: Out of memory (allocated 31457280 bytes) (tried to allocate 262144 bytes) in /in/1MXtt on line 15 mmap() failed: [12] Cannot allocate memory mmap() failed: [12] Cannot allocate memory Process exited with code 255. ``` 因此,我们需要告诉PHP在`BaseTestCase`中使用`setUp`方法,而不是使用`$this->setUp()`。为了做到这一点,我们可以像这样用`parent::setUp()`替换`$this->setUp()`: ``` class FeatureTest extends BaseTestCase { public function setUp(): void { parent::setUp(); echo 'Run extra feature test set up here...'; } } (new FeatureTest())->setUp(); // Output is: "Run base test case set up here... Run extra feature test set up here..."; ``` 现在,正如你所看到的,当我们在`FeatureTest`类中运行`setUp`方法时,我们首先运行`BaseTestCase`中的代码,然后继续运行子类中定义的其余代码。 值得注意的是,您并不总是需要将`parent::`调用放在方法的顶部。实际上,您可以将其放置在方法中任何最适合代码目的的位置。例如,如果你想先在`FeatureTest`类中运行你的代码,然后在`BaseTestCase`类中运行,你可以像这样将`parent::setUp()`调用移动到方法的底部: ## `self::`是什么? 假设我们有一个`Model`类,它有一个静态的`connection`属性和一个`makeConnection`方法。我们还可以想象我们有一个`User`类,它继承了`Model`类并覆盖了`connection`属性。 这两个类可能看起来像这样: ``` class Model { public static string $connection = 'mysql'; public function makeConnection(): void { echo 'Making connection to: '.self::$connection; } } class User extends Model { public static string $connection = 'postgres'; } ``` 现在让我们在两个类上运行`makeConnection`方法,看看我们会得到什么输出: ``` (new Model())->makeConnection(); // Output is: "Making connection to mysql" (new User())->makeConnection(); // Output is: "Making connection to mysql"; ``` 正如我们所看到的,这两个调用都导致了`Model`类的`connection`属性被使用。这是因为`self`使用了在方法所在的类上定义的属性。在这两种情况下,`makeConnection`方法在`Model`类上是打开的,因为`User`类上不存在一个方法。 为了进一步说明这一点,我们将向`User`类添加`makeConnection`方法,如下所示: ``` class Model { public static string $connection = 'mysql'; public function makeConnection(): void { echo 'Making connection to: '.self::$connection; } } class User extends Model { public static string $connection = 'postgres'; public function makeConnection(): void { echo 'Making connection to: '.self::$connection; } } ``` 现在,如果我们再次调用这两个方法,我们会得到以下输出: ``` (new Model())->makeConnection(); // Output is: "Making connection to mysql" (new User())->makeConnection(); // Output is: "Making connection to postgres"; ``` 正如您所看到的,对`makeConnection`的调用现在将使用`User`类上的`connection`字段,因为这是该方法存在的地方。 ## `static::`是什么? 现在我们已经知道了`self::`的作用,让我们来看看`static::`。 为了更好地理解它的作用,让我们更新上面的代码示例,使用`static::`而不是`self::`,如下所示: ``` class Model { public static $connection = 'mysql'; public function makeConnection() { echo 'Making connection to: '.static::$connection; } } class User extends Model { public static $connection = 'postgres'; } ``` 如果我们在两个类上运行`makeConnection`方法,我们会得到以下输出: ``` (new Model())->makeConnection(); // Output is: "Making connection to mysql" (new User())->makeConnection(); // Output is: "Making connection to postgres"; ``` 正如我们所看到的,这个输出与我们之前使用`self::$connection`时不同。对`User`类上的`makeConnection`方法的调用使用了`User`类上的`connection`属性,而不是`Model`类(该方法实际所属的类)。这是由于PHP中一个名为“后期静态绑定”的特性。 如果您有兴趣阅读更多关于后期静态绑定的内容,可以在这里查看PHP文档。https://www.php.net/manual/en/language.oop5.late-static-bindings.php > 根据PHP文档:*这个特性被命名为“后期静态绑定”,从内部的角度考虑。“后期绑定”来自这样一个事实,即static::将不会使用定义方法的类来解析,而是使用运行时信息来计算。它也被称为“静态绑定”,因为它可以用于(但不限于)静态方法调用。"* 因此,在我们的示例中,使用了`User`类上的`connection`属性,因为我们在同一个类上调用了`makeConnection`方法。 然而,值得注意的是,如果`connection`属性在`User`类上不存在,它将回退到使用`Model`类上的属性。 ## 什么时候使用self::或 static::? 现在我们对`self::`和`static::`之间的区别有了一个大致的了解,让我们快速介绍一下如何决定在自己的代码中使用哪一个。 这一切都取决于您正在编写的代码的用例。 一般来说,我通常会使用`static::`而不是`self::`,因为我希望我的类是可扩展的 例如,假设我想写一个类,我完全打算由子类继承(例如上面示例中的`BaseTestCase`类)。除非我真的想防止子类重写属性或方法,否则我想使用`static::`。 这意味着我可以有信心,如果我重写任何静态方法或字段,我的子类将使用我的重写。我无法告诉你有多少次我在代码中遇到了bug,当我在父类中使用`self::`时,然后无法弄清楚为什么我的子类没有使用我的重写! 另一方面,一些开发人员可能会争辩说,你应该坚持使用`self::`,因为你不应该真的从类继承。他们可能会建议你应该遵循“组合优于继承”的原则。我不会深入研究这个话题,因为这是未来的另一篇博客文章。但从广义上说,简单地说,这个原则指出,你应该避免通过将所有逻辑放在父类中来为类添加功能,而是通过用许多更小的类来构建类来添加功能。 这意味着如果你遵循这个原则,你就不需要使用`static::`,因为你永远不会扩展你的父类。如果你想确保类不能被扩展,你甚至可以更进一步,在定义类时使用`final`关键字。使用`final`关键字可以防止类被继承,所以它可以减少您对类可能意外扩展并引入任何潜在错误的担忧。 一般来说,最好在编写代码时根据具体情况决定应该使用`static::`还是`self::`。