CatchAdmin PHP 后台管理框架 Logo CatchAdmin

static::class 与 self::class 的差别远比想象中重要

很多 PHP 开发者在写下这两段代码时并不会多想:

php
class Repository {
    public function modelClass(): string {
        return self::class;
    }
}

class Repository {
    public function modelClass(): string {
        return static::class;
    }
}

在同一份文件、独立运行时,它们返回的字符串完全一致,都是 "Repository"。测试通过,代码评审放行,大家继续往下推进。

直到有人给 Repository 加上了子类,比如 UserRepository extends Repository。此时第一种写法仍旧返回 "Repository",第二种则会返回 "UserRepository"。根据用到这个值的场景,这段代码要么是一个完全通用的基类,要么就是一个只在子类存在时才会暴露的 bug——而子类真正存在的地方往往是生产环境。

本文会深入探讨 PHP 中 staticself 的差别。重点不是语法,而是语义:何时该用哪个、选错时会出什么问题,以及这个选择会在哪些模式里被放大——工厂方法、仓储基类、事件类、Eloquent scope、带 clone with 语义的 DTO 等等。两个关键字之间的差别很小,引发的 bug 却一点都不小。

TL;DR 速览

  • self:: 指向代码所在的类,在解析/编译期就被锁定。
  • static:: 指向实际被调用的类,在运行时通过后期静态绑定(LSB)解析。
  • 在基类方法中,self::class 返回基类名;static::class 在子类调用时返回子类名。
  • 选错不会报错,只会悄悄返回错误的类、错误的实例、或者错误的常量——而这种 bug 只会在有人继承代码时显现。
  • 经验法则:如果写的是需要被继承的代码,默认使用 static::,除非确实想把行为锁定在当前类。
  • self::classstatic::class 以及对象上的 ::class(PHP 8+ 的 $obj::class)各自有独立的语义,混用就是隐蔽 bug 的温床。

本文要点

  • 后期静态绑定的精确机理——写下 static:: 时 PHP 在运行时究竟做了什么
  • 真正会改变行为的六个场景(不只是类名,还有常量、方法、属性、new、类型提示、克隆)
  • 决定程序正确性的现实模式:工厂、仓储、事件、DTO、流式构建器
  • PHP 8+ 新增特性($object::classnew static(...)、克隆语义)与 LSB 的联动
  • 性能影响——确实存在,但几乎都不是关键
  • 一套决策框架:何时选用哪个关键字,以及 self:: 客观更合适的唯一场景

真正能记住的思维模型

先把关键字本身放一边,想一想它们各自在回答什么问题。

self:: 回答的是:"我写在哪个类里?" 答案在 PHP 解析文件的那一刻就固定下来,永远不变。

static:: 回答的是:"最外层的 new 或方法调用是在哪个类上发起的?" 答案取决于运行时上下文,每次调用都可能不同。

当写下 class Foo 并在其中放一个方法时,这个方法属于 Foo。方法体内的 self:: 永远指向 Foo。即便有人写 class Bar extends Foo 并在 Bar 实例上调用这个方法,方法的定义仍在 Foo,所以 self:: 依旧解析为 Foo

static:: 的行为完全不同。PHP 会记录调用是从哪个类发起的。调用 Bar::theMethod() 时,PHP 会沿继承链向上找到 Foo 里的实现,但它记得这次调用源自 Bar。于是方法内部的 static:: 解析为 Bar

这就是"后期静态绑定"。"后期"是指解析发生得很晚——在调用时才确定,而不是解析时;"静态"是指它作用于静态式的类引用。

用代码来演示:

php
class Animal {
    public function whoAmI(): void {
        echo "self::class    = " . self::class . "\n";
        echo "static::class  = " . static::class . "\n";
    }
}

class Dog extends Animal {}

(new Animal)->whoAmI();
// self::class    = Animal
// static::class  = Animal

(new Dog)->whoAmI();
// self::class    = Animal       ← 写在 Animal 中,永远锁定 Animal
// static::class  = Dog          ← 从 Dog 调起,解析为 Dog

全部秘密就是这一段。本文后面讲的所有场景,都是这一机制的自然推论。

会咬人的六个真实场景

两个关键字的差异只会在继承关系中才有意义。下面是六个会真正改变程序行为的场景,按从常见到微妙排序。

1. new selfnew static——工厂方法的经典陷阱

这是最经典的一例。基类里有一个静态工厂方法:

php
class Model {
    public static function make(array $data): Model {
        return new self($data);     // ← 永远创建 Model,无法创建子类
    }

    public function __construct(protected array $data) {}
}

class User extends Model {}

$user = User::make(['name' => 'Alice']);
var_dump($user instanceof User);    // false!
var_dump($user instanceof Model);   // true

new self() 把被实例化的类硬编码为 Model。哪怕调用方写的是 User::make(...),拿到的仍是 Model。这种结果几乎从来不是想要的。

修复方式是改成 new static

php
class Model {
    public static function make(array $data): static {
        return new static($data);   // ← 按调用方所在的类实例化
    }

    public function __construct(protected array $data) {}
}

class User extends Model {}

$user = User::make(['name' => 'Alice']);
var_dump($user instanceof User);    // true
var_dump($user instanceof Model);   // true

同时返回类型也从 Model 改为 static。PHP 提供了一个特殊的 static 返回类型,含义是"返回值是实际被调用的那个类"。如果保留 Model,PHPStan、Psalm 这类静态分析器会抱怨 make() 声称返回 Model,实际上返回的是子类。改为 : static 之后,工具与读者就都理解了这层语义。

在真实项目里,这决定了一个作为基类的 Repository 到底是能适配所有实体,还是会静默地返回无用的 Repository 实例。Laravel 的 Model::create() 能够多态地工作,答案就在这里——Eloquent 的工厂方法内部都使用 static

2. 常量(self::CONSTstatic::CONST

PHP 5.3 为方法引入了后期静态绑定,常量则是后来跟进的,但遵循同样的规则:self:: 锁定,static:: 延迟解析。

php
class Config {
    const PREFIX = 'default';

    public function fullKey(string $key): string {
        return self::PREFIX . ':' . $key;       // 永远是 'default:...'
    }

    public function fullKeyDynamic(string $key): string {
        return static::PREFIX . ':' . $key;     // 若子类重新声明了常量,则使用子类的值
    }
}

class RedisConfig extends Config {
    const PREFIX = 'redis';
}

$c = new RedisConfig();
echo $c->fullKey('user');         // "default:user"       ← 出乎意料
echo $c->fullKeyDynamic('user');  // "redis:user"         ← 符合预期

这一类问题格外隐蔽,因为常量给人的直觉是"天然就应该多态"。事实并非如此。self:: 引用的常量在解析期就冻结为代码所在类的值。

当希望基类定义默认常量、允许子类覆盖时,必须使用 static::

php
class Event {
    const VERSION = 1;

    public function toArray(): array {
        return [
            'type'    => static::class,
            'version' => static::VERSION,       // 子类可覆盖
            'at'      => (new DateTimeImmutable)->format(DATE_ATOM),
        ];
    }
}

class UserCreatedEvent extends Event {
    const VERSION = 3;    // 该子类的事件版本已经演进到 v3
}

如果写成 self::VERSION,无论子类怎么声明,所有子类事件都会上报 v1。安静、令人困惑,属于那种半年后调查事件 schema 不一致时才会被发现的 bug。

3. 可被覆盖的静态方法

同样的原则也适用于静态方法之间的互相调用:

php
class Query {
    public static function tableName(): string {
        return 'default';
    }

    public static function all(): array {
        $table = self::tableName();      // ← 硬绑定到 Query::tableName()
        return DB::select("SELECT * FROM $table");
    }
}

class Users extends Query {
    public static function tableName(): string {
        return 'users';
    }
}

Users::all();   // SELECT * FROM default——并非期望结果

self::tableName() 根本不关心调用方其实是 Users::all()。因为它写在 Query 里,self:: 就意味着 Query,于是调用的是 Query::tableName()。子类的覆盖被完全绕过。

改为 static::

php
class Query {
    public static function tableName(): string {
        return 'default';
    }

    public static function all(): array {
        $table = static::tableName();    // ← 存在子类覆盖时优先使用
        return DB::select("SELECT * FROM $table");
    }
}

class Users extends Query {
    public static function tableName(): string {
        return 'users';
    }
}

Users::all();   // SELECT * FROM users——符合预期

Laravel 的查询构建器与 Eloquent scope 的内部实现正是这种形态。基类搭建了一个由 static:: 构成的脚手架,子类按需填入自己的值。

4. instanceof 与类型检查

一个不太显眼的细节:在方法内部,instanceof selfinstanceof static 含义不同。

php
class Animal {
    public function isSameType(object $other): bool {
        return $other instanceof self;      // 任何 Animal 或其子类均为 true
    }

    public function isExactType(object $other): bool {
        return $other instanceof static;    // 仅当具体类相同才为 true
    }
}

class Dog extends Animal {}
class Cat extends Animal {}

$dog = new Dog();
$anotherDog = new Dog();
$cat = new Cat();

$dog->isSameType($cat);         // true——都属于 Animal
$dog->isExactType($cat);        // false——Cat 不是 Dog
$dog->isSameType($anotherDog);  // true
$dog->isExactType($anotherDog); // true

instanceof self 的语义是"它是否与声明类同属一个继承层次";instanceof static 的语义是"它是否恰好是被调用的那个具体类"。

对一类需要拒绝跨类型比较的 DTO 相等性判断,通常想要 static——前提是可以接受"子类视为不同类型"。若要实现子类也算匹配的多态相等判断,则应使用 self(或 instanceof self)。

5. 带修改的克隆(clonestatic

PHP 8.5 引入了 clone($object, [...]) 语法(以及此前常见的"手动 clone + 覆盖"写法),它们与 static 的组合值得特别理解。

DTO 里常见的模式:

php
abstract class DTO {
    public function with(array $changes): static
    {
        $clone = clone $this;
        foreach ($changes as $key => $value) {
            $clone->$key = $value;
        }
        return $clone;
    }
}

final class UserDTO extends DTO {
    public function __construct(
        public string $name,
        public string $email,
    ) {}
}

$u  = new UserDTO('Alice', 'alice@example.com');
$u2 = $u->with(['name' => 'Alicia']);
var_dump($u2 instanceof UserDTO);   // true

如果没有 : static 返回类型,静态分析器会推断 with() 返回 DTO,继而把 $u2->email 标注为"属性可能不存在于 DTO 上"。加上 : static 后,工具与读者都明白:对 UserDTO 调用 with() 得到的仍是 UserDTO

clone 自身并不使用 static 关键字的语义(它克隆的是对象的实际运行时类型),但"克隆并修改"这类方法的返回类型应当是 static,才能让继承关系自然工作。

6. Trait 方法与 $this::class

Trait 带来一种更微妙的情况。在 trait 内部,self:: 指向使用该 trait 的类,而非 trait 本身(因为 trait 在运行时并不具备独立的类身份)。static:: 则仍如常工作:

php
trait HasName {
    public function describe(): string {
        return static::class . ' has some name';   // 解析为使用该 trait 的类
    }

    public function describeSelf(): string {
        return self::class . ' has some name';     // 同样是使用该 trait 的类——trait 没有独立类身份
    }
}

class Dog {
    use HasName;
}

class Puppy extends Dog {}

(new Dog)->describe();       // "Dog has some name"
(new Dog)->describeSelf();   // "Dog has some name"
(new Puppy)->describe();     // "Puppy has some name"     ← 后期静态绑定
(new Puppy)->describeSelf(); // "Dog has some name"       ← self = 声明类 Dog

这会让一部分开发者栽跟头,他们以为 trait 与继承的行为一致。并非如此。在 trait 里,self 的含义是"使用该 trait 的那个类"(此例中是直接使用者 Dog);static 仍然是"被调用的那个类"(此例中的 Puppy)。如果 trait 的方法需要在使用者的子类之间多态地工作,必须使用 static::

PHP 8+ 还新增了 $object::class,作为 get_class($object) 的便捷写法:

php
$animal = new Dog();
echo $animal::class;    // "Dog"

这是运行时等价物。它永远返回对象的实际运行时类,与代码所在位置、调用方式都无关。对于已经持有的局部对象变量,$obj::class 是最清爽的写法。

需要内化的三种关键字分工

绝大多数现实决策都归结为在三样东西之间作选择:

  • self::class——"我写在哪个类里,编译期就锁定。" 适合真正需要指向声明类、且不希望子类覆盖的场景。使用场景较少,合理的例子包括:不应多态化的内部工具方法,以及需要在整个继承层次保持稳定的常量。
  • static::class——"调用发起于哪个类,在运行时解析。" 当方法本身是可继承的、子类应能影响其行为时使用。这是多数基类代码的默认选择:工厂、仓储模式、抽象框架方法。
  • $object::class——"这个具体对象实例的运行时类。" 当手上已经握有一个具体对象、需要得到其精确类名时使用,等价于 get_class($object),但更易读。在所有 PHP 8+ 代码中都可用。

下面的示例把三者同时展现出来:

php
abstract class Serializer {
    public static function typeId(): string {
        return static::class;       // 运行时子类名
    }

    public function debug(): array {
        return [
            'declared_in' => self::class,      // 始终为 "Serializer"
            'called_on'   => static::class,    // 实际子类
            'instance_of' => $this::class,     // 实际子类(此处与 static 一致)
        ];
    }
}

class JsonSerializer extends Serializer {}

$s = new JsonSerializer();
print_r($s->debug());
// Array (
//     [declared_in] => Serializer
//     [called_on]   => JsonSerializer
//     [instance_of] => JsonSerializer
// )

在具体对象的实例方法里,static::class$this::class 通常一致。三者真正分叉的场景出现在静态方法中(那里没有 $this),或者需要在不了解继承关系的前提下检查传入对象的类时。

一个被注册表模式反噬的真实案例

下面用一个改写自真实生产事故的例子,展示这类 bug 的形态。

某个代码库有一个带注册表的基类 Command

php
abstract class Command {
    protected static array $handlers = [];

    public static function register(callable $handler): void {
        self::$handlers[static::class] = $handler;
    }

    public static function dispatch(Command $command): mixed {
        $class   = $command::class;
        $handler = self::$handlers[$class] ?? null;

        if (! $handler) {
            throw new RuntimeException("No handler for $class");
        }

        return $handler($command);
    }
}

class CreateUserCommand extends Command {}
class DeleteUserCommand extends Command {}

注册是没问题的。CreateUserCommand::register(fn($c) => ...) 正确地把处理器写入 self::$handlers['CreateUserCommand']——因为 register() 内部的 static::class 解析为 CreateUserCommand

真正的陷阱很隐蔽。self::$handlers 里的 self 指向 Command 这个类——这其实正是一个共享注册表所需要的(整棵继承树共用一份注册表),处理器被正确地保存了下来。

那么 bug 出在哪?原始代码里并没有。真正让 bug 浮出水面的,是有人加入中间层继承:

php
abstract class UserCommand extends Command {
    // 中间类,目前无方法体
}

class CreateUserCommand extends UserCommand {}

然后有人在 UserCommand 里自作聪明:

php
abstract class UserCommand extends Command {
    protected static array $handlers = [];   // 覆盖了父类属性

    public static function register(callable $handler): void {
        self::$handlers[static::class] = $handler;
    }
}

他们在 UserCommand 里再次声明了 $handlers,以为这会成为一份专属于用户命令的注册表。问题在于:此时 self::$handlers 指向 UserCommand::$handlers(解析期就绑定到了方法所在的那个类),于是注册进入的数组和派发时读取的数组就成了两个不同的数组。

修复方式是选定唯一一层来承载注册表,并始终通过它访问。然而定位这个问题花了数小时,留下的只有"为什么处理器注册成功,派发却找不到?"的困惑。答案是:两处的 self:: 指向了两个不同的数组。

核心教训是:self::$property 并不像实例属性那样具备多态性。如果基类里有静态属性,而子类又重新声明了同名属性,self:: 会根据方法所在类的不同指向不同的存储。要么把静态属性严格控制在继承层次的某一层里,要么显式改用 static::,让多态性在代码中可见。

工厂方法的标准模板

由于这一模式反复出现,下面给出可继承基类工厂方法的推荐模板:

php
abstract class AggregateRoot {
    private function __construct(
        protected readonly string $id,
        protected array $state = [],
    ) {}

    /**
     * 使用新生成的标识符创建聚合根。
     * 返回调用方对应子类的实例。
     */
    public static function create(array $initialState = []): static
    {
        return new static(
            id: self::generateId(),   // ← self:基类自身的辅助函数
            state: $initialState,
        );
    }

    /**
     * 从持久化状态还原聚合根。
     * 返回调用方对应子类的实例。
     */
    public static function fromState(string $id, array $state): static
    {
        return new static($id, $state);
    }

    private static function generateId(): string
    {
        // 此处永远不需要多态性——使用 self 既正确又表达了意图
        return bin2hex(random_bytes(16));
    }
}

final class Order   extends AggregateRoot {}
final class Invoice extends AggregateRoot {}

$order   = Order::create(['status' => 'pending']);     // Order
$invoice = Invoice::create(['total' => 500]);          // Invoice

这里有三处值得注意:

  • new static($id, $state)——工厂之所以能够多态,靠的就是这一句。Order::create() 返回 OrderInvoice::create() 返回 Invoice
  • : static 返回类型——这是写给静态分析器和人类读者看的契约,明确"返回值就是被调用的那个类"。
  • self::generateId()——这里特意使用 self::。ID 生成算法不应被子类覆盖。子类若有需要当然可以重新声明 generateId(),但基类自身的 create() 必须始终调用基类的 ID 生成器,不受子类影响。这是一个 self::static:: 更贴切的少见场景。

心智上的判断标准始终一致:是否希望子类能改写这段行为?是则使用 static::,否则使用 self::

性能:可以忽略但值得知道

偶尔有人声称 self::static:: 更快,理由是前者在编译期解析。严格来说成立,但其影响几乎可以忽略。

后期静态绑定的开销在每次调用的纳秒级别。即便在每秒循环调用一千万次的热点场景下,从 static:: 换成 self:: 也不过节省几百毫秒。除非在解析器、物理引擎或框架深处的热点循环里,否则这点差别根本不会被察觉。

比 LSB 开销更重要的是:选用的关键字是否保证了行为正确。一个"更快"却返回了错误类的程序,不能算真正的更快。

不过,有一类性能模式确实值得留意:热点循环里的 static::CONST 会在每次迭代都做一次运行时解析。如果常量在当前类里确实不变,就应该先缓存下来:

php
// 较慢——每次迭代都解析 static::VERSION
foreach ($items as $item) {
    $item->tag = static::VERSION;
}

// 较快——只解析一次,后续复用
$version = static::VERSION;
foreach ($items as $item) {
    $item->tag = $version;
}

这属于只在特定热点路径上生效的微优化。一个每秒只跑十次的请求处理器,完全用不到。

与该主题相关的 PHP 8+ 特性

现代 PHP 的一些特性与 static / self 组合得很好。

static 返回类型(PHP 8.0+)——专门用于表达"本方法返回的是被调用的那个类":

php
public function with(array $changes): static { ... }

在该类型出现之前,只能靠 @return static docblock 充当替代。如今它已成为正式类型,任何返回 new static(...)clone $this 的方法都应使用它。

new $class(...) 动态构造——当 $class 是运行时字符串时,selfstatic 都不适用,被实例化的类就是字符串所表示的那个类:

php
public function makeFromClass(string $class, array $data): object {
    return new $class($data);   // 字符串写什么,类就是什么
}

只读属性(PHP 8.1+)与 static 返回类型的组合在不可变值对象上尤其契合——返回类型正确表达"拿回的仍然是同一个子类",readonly 修饰符又杜绝了意外写入。这是 DTO 与值对象的现代写法。

一等可调用语法(PHP 8.1+)——static::method(...) 会在调用时把可调用对象绑定到当前类,遵循后期静态绑定;self::method(...) 则绑定在声明类上。规则一致,只是换了一种表达方式:

php
abstract class Formatter {
    public function pipeline(): array {
        return [
            static::class . '::normalize',   // 延迟解析,子类可覆盖
            static::normalize(...),          // 等效,写成 callable 形式
            self::class . '::escape',        // 锁定到 Formatter::escape
        ];
    }

    protected static function normalize(string $s): string { return trim($s); }
    protected static function escape(string $s): string   { return htmlspecialchars($s); }
}

决策框架

写下 self::static:: 之前,可以依次自问下列问题。

  1. 这段代码是否位于 final 类或永远不会被继承的类?——二者都可用,约定上选 self:: 以表达意图。该类不会有子类,区别在此没有实际后果。
  2. 这段代码是否位于打算被继承的基类?——几乎总是 static::。例外情况是被引用的对象属于实现细节,不应被多态化(比如前文的 generateId())。
  3. 这是一个实例方法,且手头有一个具体对象?——优先使用 $this::class$this->method() 作运行时类型查找,表达最直接。
  4. 是否在写一个应返回子类实例的工厂方法?——始终使用 new static(...),并配 : static 返回类型。
  5. 是否覆盖了常量并期望基类能看到这次覆盖?——必须使用 static::CONSTself::CONST 会悄无声息地返回基类的值。
  6. 阅读既有代码时遇到 self::,并且正准备继承它?——仔细辨析原作者到底是有意"锁定在当前类",还是只是习惯性地写了 self::。两者都很常见。

简明 Q&A

Q:有没有场景 self:: 严格优于 static::
有三种。第一,在禁止继承的 final 类里,self:: 向读者传达了"无需考虑子类"的意图;第二,方法体依赖某个绝不可被覆盖的具体实现——少见但确实存在;第三,私有方法本就无法被覆盖,用 self:: 更能忠实表达意图。除此之外,static:: 更安全。

Q:parent:: 是什么语义,与这套机制如何衔接?
parent:: 指向代码所在类的父类——是 self:: 的"向上"版本。它同样在解析期确定,主要用于调用 parent::__construct() 或扩展父类方法。它没有对应的延迟绑定形式(不存在 static_parent::),这在某些场合会让人略感不便。

Q:Laravel 模型里到处都是 new self(...),是不是有问题?
多数情况不会有问题,因为 Eloquent 内部处理了大部分细节——它的 newInstance() 使用 static::。但如果在模型上自定义了工厂方法并在其中写了 new self(...),一旦需要继承(例如单表继承或类似模式),这些方法就会失效。应逐一审查并替换为 new static(...): static 返回类型。

Q:static:: 能引用非静态的成员吗?
可以。"后期静态绑定"中的 static 指的是解析机制,而不是被引用对象的静态性。static::someMethod() 既能调用静态方法,也能调用非静态方法(不过以静态方式调用非静态方法通常是错误用法)。更常见的用法是 static::class——取得运行时类名后交由其他 API 使用。

Q:如何通过测试捕获这类 bug?
最可靠的方式是在测试里给基类写一个子类,再验证方法返回的是子类实例,或常量解析为子类的值。仅覆盖基类自身的测试无法捕获 self::static:: 的差异——这种差异只在继承中显现。这恰恰是"只对基类做隔离测试"会给人虚假信心的一类场景。

Q:PHPStan、Psalm 对此怎么看?
两者都理解 static 返回类型,能在方法声明 : static 时正确推断子类类型。它们(在较严格的级别下)还会对非 final 类里的 new self(...) 提示,建议改为 new static(...)。把 PHPStan 调到 level 6+、Psalm 调到 level 3+,多数此类问题会被自动暴露。

Q:这件事只与面向对象相关吗?还是在函数式/过程式代码中也有影响?
只与 OOP 相关,更具体地说只与继承相关。如果代码库里没有类继承——所有类都 final、靠组合而非继承——selfstatic 的差别对实际开发就只是学术话题。final 类里统一用 self:: 保持一致即可。差异只有在一个类继承另一个类时才有实质意义。

结语

self::static:: 之间的差别,是 PHP 语言逐步演进所留下的痕迹。在后期静态绑定(PHP 5.3)出现之前,self:: 是唯一选择,静态方法的继承在当时几乎是坏的——根本无法写出真正多态的工厂。LSB 修好了这件事,却让两个关键字同时保留下来,留给开发者自行判断用哪一个。

实用原则是:在基类里拿不准时,优先 static::。它是让代码在被继承时依旧正确运行的关键字。self:: 是一把更窄的工具,服务于更窄的目的——把行为锁死在声明类,通常用于 final 类或内部辅助方法。

这个决定很小,仅仅两个字符的差别。但和所有与多态性相关的决定一样,它的影响会累积。一个始终使用 static:: 的基类会成为子类的一块干净底座;一个习惯性写 self:: 的基类,则会成为所有子类都会静默出错、且只有在生产环境才暴露的温床。

因此要有意识地选择、审计既有代码、在每一个工厂或流式方法上都加 : static 返回类型。那些因此未被写出的 bug,所节省的代价远超花在思考上的时间。

速查卡

心智模型:self:: 回答"代码写在哪里",static:: 回答"调用来自哪里"。

工厂方法的标准形态:

php
public static function create(...): static {
    return new static(...);
}

差异会产生影响的六个位置:

  • new selfnew static——工厂方法与多态
  • self::CONSTstatic::CONST——需要被子类覆盖的常量
  • self::method()static::method()——调用可被覆盖的辅助方法
  • instanceof selfinstanceof static——基类内的类型检查
  • 返回类型 : self: static——静态分析器所理解的契约
  • Trait 内部——self 指向使用该 trait 的类,依旧在解析期锁定

三元查找对照:

  • self::class——代码声明所在类,编译期
  • static::class——调用发起的类,运行时(后期静态绑定)
  • $object::class——具体对象的运行时类(PHP 8+)

final 类里两者都可用,约定选 self::,因为继承已不可能发生。在打算被继承的基类里,默认选 static::。例外是那些属于"不应被覆盖的实现细节"的地方——此时 self:: 能把意图清楚地传达给下一位读者。

本作品采用《CC 协议》,转载必须注明作者和本文链接