static::class 与 self::class 的差别远比想象中重要
很多 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 中 static 与 self 的差别。重点不是语法,而是语义:何时该用哪个、选错时会出什么问题,以及这个选择会在哪些模式里被放大——工厂方法、仓储基类、事件类、Eloquent scope、带 clone with 语义的 DTO 等等。两个关键字之间的差别很小,引发的 bug 却一点都不小。
TL;DR 速览
self::指向代码所在的类,在解析/编译期就被锁定。static::指向实际被调用的类,在运行时通过后期静态绑定(LSB)解析。- 在基类方法中,
self::class返回基类名;static::class在子类调用时返回子类名。 - 选错不会报错,只会悄悄返回错误的类、错误的实例、或者错误的常量——而这种 bug 只会在有人继承代码时显现。
- 经验法则:如果写的是需要被继承的代码,默认使用
static::,除非确实想把行为锁定在当前类。 self::class、static::class以及对象上的::class(PHP 8+ 的$obj::class)各自有独立的语义,混用就是隐蔽 bug 的温床。
本文要点
- 后期静态绑定的精确机理——写下
static::时 PHP 在运行时究竟做了什么 - 真正会改变行为的六个场景(不只是类名,还有常量、方法、属性、
new、类型提示、克隆) - 决定程序正确性的现实模式:工厂、仓储、事件、DTO、流式构建器
- PHP 8+ 新增特性(
$object::class、new 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。
这就是"后期静态绑定"。"后期"是指解析发生得很晚——在调用时才确定,而不是解析时;"静态"是指它作用于静态式的类引用。
用代码来演示:
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 self 与 new static——工厂方法的经典陷阱
这是最经典的一例。基类里有一个静态工厂方法:
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); // truenew self() 把被实例化的类硬编码为 Model。哪怕调用方写的是 User::make(...),拿到的仍是 Model。这种结果几乎从来不是想要的。
修复方式是改成 new static:
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::CONST 与 static::CONST)
PHP 5.3 为方法引入了后期静态绑定,常量则是后来跟进的,但遵循同样的规则:self:: 锁定,static:: 延迟解析。
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:::
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. 可被覆盖的静态方法
同样的原则也适用于静态方法之间的互相调用:
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:::
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 self 和 instanceof static 含义不同。
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); // trueinstanceof self 的语义是"它是否与声明类同属一个继承层次";instanceof static 的语义是"它是否恰好是被调用的那个具体类"。
对一类需要拒绝跨类型比较的 DTO 相等性判断,通常想要 static——前提是可以接受"子类视为不同类型"。若要实现子类也算匹配的多态相等判断,则应使用 self(或 instanceof self)。
5. 带修改的克隆(clone 与 static)
PHP 8.5 引入了 clone($object, [...]) 语法(以及此前常见的"手动 clone + 覆盖"写法),它们与 static 的组合值得特别理解。
DTO 里常见的模式:
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:: 则仍如常工作:
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) 的便捷写法:
$animal = new Dog();
echo $animal::class; // "Dog"这是运行时等价物。它永远返回对象的实际运行时类,与代码所在位置、调用方式都无关。对于已经持有的局部对象变量,$obj::class 是最清爽的写法。
需要内化的三种关键字分工
绝大多数现实决策都归结为在三样东西之间作选择:
self::class——"我写在哪个类里,编译期就锁定。" 适合真正需要指向声明类、且不希望子类覆盖的场景。使用场景较少,合理的例子包括:不应多态化的内部工具方法,以及需要在整个继承层次保持稳定的常量。static::class——"调用发起于哪个类,在运行时解析。" 当方法本身是可继承的、子类应能影响其行为时使用。这是多数基类代码的默认选择:工厂、仓储模式、抽象框架方法。$object::class——"这个具体对象实例的运行时类。" 当手上已经握有一个具体对象、需要得到其精确类名时使用,等价于get_class($object),但更易读。在所有 PHP 8+ 代码中都可用。
下面的示例把三者同时展现出来:
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:
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 浮出水面的,是有人加入中间层继承:
abstract class UserCommand extends Command {
// 中间类,目前无方法体
}
class CreateUserCommand extends UserCommand {}然后有人在 UserCommand 里自作聪明:
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::,让多态性在代码中可见。
工厂方法的标准模板
由于这一模式反复出现,下面给出可继承基类工厂方法的推荐模板:
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()返回Order,Invoice::create()返回Invoice。: static返回类型——这是写给静态分析器和人类读者看的契约,明确"返回值就是被调用的那个类"。self::generateId()——这里特意使用self::。ID 生成算法不应被子类覆盖。子类若有需要当然可以重新声明generateId(),但基类自身的create()必须始终调用基类的 ID 生成器,不受子类影响。这是一个self::比static::更贴切的少见场景。
心智上的判断标准始终一致:是否希望子类能改写这段行为?是则使用 static::,否则使用 self::。
性能:可以忽略但值得知道
偶尔有人声称 self:: 比 static:: 更快,理由是前者在编译期解析。严格来说成立,但其影响几乎可以忽略。
后期静态绑定的开销在每次调用的纳秒级别。即便在每秒循环调用一千万次的热点场景下,从 static:: 换成 self:: 也不过节省几百毫秒。除非在解析器、物理引擎或框架深处的热点循环里,否则这点差别根本不会被察觉。
比 LSB 开销更重要的是:选用的关键字是否保证了行为正确。一个"更快"却返回了错误类的程序,不能算真正的更快。
不过,有一类性能模式确实值得留意:热点循环里的 static::CONST 会在每次迭代都做一次运行时解析。如果常量在当前类里确实不变,就应该先缓存下来:
// 较慢——每次迭代都解析 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+)——专门用于表达"本方法返回的是被调用的那个类":
public function with(array $changes): static { ... }在该类型出现之前,只能靠 @return static docblock 充当替代。如今它已成为正式类型,任何返回 new static(...) 或 clone $this 的方法都应使用它。
new $class(...) 动态构造——当 $class 是运行时字符串时,self 与 static 都不适用,被实例化的类就是字符串所表示的那个类:
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(...) 则绑定在声明类上。规则一致,只是换了一种表达方式:
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:: 之前,可以依次自问下列问题。
- 这段代码是否位于
final类或永远不会被继承的类?——二者都可用,约定上选self::以表达意图。该类不会有子类,区别在此没有实际后果。 - 这段代码是否位于打算被继承的基类?——几乎总是
static::。例外情况是被引用的对象属于实现细节,不应被多态化(比如前文的generateId())。 - 这是一个实例方法,且手头有一个具体对象?——优先使用
$this::class或$this->method()作运行时类型查找,表达最直接。 - 是否在写一个应返回子类实例的工厂方法?——始终使用
new static(...),并配: static返回类型。 - 是否覆盖了常量并期望基类能看到这次覆盖?——必须使用
static::CONST。self::CONST会悄无声息地返回基类的值。 - 阅读既有代码时遇到
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、靠组合而非继承——self 与 static 的差别对实际开发就只是学术话题。final 类里统一用 self:: 保持一致即可。差异只有在一个类继承另一个类时才有实质意义。
结语
self:: 与 static:: 之间的差别,是 PHP 语言逐步演进所留下的痕迹。在后期静态绑定(PHP 5.3)出现之前,self:: 是唯一选择,静态方法的继承在当时几乎是坏的——根本无法写出真正多态的工厂。LSB 修好了这件事,却让两个关键字同时保留下来,留给开发者自行判断用哪一个。
实用原则是:在基类里拿不准时,优先 static::。它是让代码在被继承时依旧正确运行的关键字。self:: 是一把更窄的工具,服务于更窄的目的——把行为锁死在声明类,通常用于 final 类或内部辅助方法。
这个决定很小,仅仅两个字符的差别。但和所有与多态性相关的决定一样,它的影响会累积。一个始终使用 static:: 的基类会成为子类的一块干净底座;一个习惯性写 self:: 的基类,则会成为所有子类都会静默出错、且只有在生产环境才暴露的温床。
因此要有意识地选择、审计既有代码、在每一个工厂或流式方法上都加 : static 返回类型。那些因此未被写出的 bug,所节省的代价远超花在思考上的时间。
速查卡
心智模型:self:: 回答"代码写在哪里",static:: 回答"调用来自哪里"。
工厂方法的标准形态:
public static function create(...): static {
return new static(...);
}差异会产生影响的六个位置:
new self与new static——工厂方法与多态self::CONST与static::CONST——需要被子类覆盖的常量self::method()与static::method()——调用可被覆盖的辅助方法instanceof self与instanceof static——基类内的类型检查- 返回类型
: self与: static——静态分析器所理解的契约 - Trait 内部——
self指向使用该 trait 的类,依旧在解析期锁定
三元查找对照:
self::class——代码声明所在类,编译期static::class——调用发起的类,运行时(后期静态绑定)$object::class——具体对象的运行时类(PHP 8+)
在 final 类里两者都可用,约定选 self::,因为继承已不可能发生。在打算被继承的基类里,默认选 static::。例外是那些属于"不应被覆盖的实现细节"的地方——此时 self:: 能把意图清楚地传达给下一位读者。