9. 特质
特质(Trait)是类似于类的结构。不过,它们无法被实例化。它们旨在被包含在类中。将一个特质包含到类中,其效果等同于将特质的代码复制到类中。我们通常将可能在多个类中重复使用的代码放入特质中。
9.1. 脚本树结构

9.2. 在类中包含一个特质
脚本 [traits-01.php] 展示了在类中使用特性的基本方法:
<?php
class Class04 {
// 属性
private $name;
// 构造函数
public function __construct(string $name) {
$this->name = $name;
}
// 获取器和设置器
public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->name = $name;
}
}
trait Trait04 {
// 属性
private $name;
// 获取器和设置器
public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->name = $name;
}
}
class Class05 {
// 特性的包含
use Trait04;
// 构造函数
public function __construct(string $name) {
$this->name = $name;
}
}
// 测试 --------------
$class04 = new Class04("Tim");
$class05 = new Class05("Burton");
print $class04->getName() . "\n";
print $class05->getName() . "\n";
// 两个类的显示
print_r($class04);
print_r($class05);
代码注释
- 第3-21行:定义类[Class04],包含一个属性、其get/set方法以及一个构造函数;
- 第23-36行:将[Class04]的代码(不含构造函数)原样复制到[Trait04]特质中。由于特质无法实例化,因此不包含构造函数;
- 第 23 行:正是关键字 [trait] 使 [Trait04] 成为一个特质而非类;
- 第 38-45 行: 定义了一个类 [Class05],该类继承了特性 [Trait04] 的代码(第 40 行),并为其添加了一个构造函数(第 43-45 行),该构造函数与类 [Class04] 的构造函数相同,以使该类可实例化;
- 第 40 行:关键字 [use] 用于将一个 Trait 包含到类中;
- 第50-56行:测试表明,类[Class04]和[Class05]的工作方式相同;
结果
第3-10行的结果表明,类[Class04]和[Class05]的内容相同;
结论
在类中使用 [use Trait] 语句,等同于将 [Trait] 的代码包含在该类中。
9.3. 在不同类中使用相同的特质
特性的首要优势似乎在于可以在不同类之间复用相同的代码(属性+方法)。不过,我们将看到,使用简单的类也能达到同样的目的。
以下脚本 [trait-02.php] 演示了在类之间共享 trait 的情况:
<?php
trait Trait01 {
// 属性
private $id = 0;
// 方法
public function doSomething() {
print "Trait01::doSomething… ($this->id)\n";
}
}
class Class02 {
// 包含 Trait01
use Trait01 {
// 方法 [Trait01::doSomething] 可用
// 在类中以名称 [doSomethingInTrait] 存在
Trait01::doSomething as doSomethingInTrait;
}
// 该类专有的方法
public function doSomething(): void {
// id 属性
$this->id += 10;
// 调用Trait01方法
$this->doSomethingInTrait();
// 本地显示
print "Class02->doSomething\n";
}
}
class Class03 {
// 包含 Trait01
use Trait01;
// 类本地方法
public function doSomethingElse(): void {
// ID属性
$this->id += 10;
// Trait01 方法的使用
$this->doSomething();
// 本地显示
print "Class03->doSomethingElse\n";
}
}
// test01 ----------------
function test01(): void {
$class02 = new Class02();
$class03 = new Class03();
$class02->doSomething();
$class03->doSomethingElse();
}
// test01
print "test01-----------------\n";
test01();
注释
- 第 3-10 行:一个特质,定义了一个属性(第 5 行)和一个方法(第 8-10 行)。
- [Trait01] 特质被注入到两个类中:[Class02](第 14-32 行)和 [Class03](第 34-46 行)。
- 第16-20行:将[Trait01]注入到[Class02]中;
- 第 19 行旨在解决冲突:[Trait01] 和 [Class02] 都包含一个名为 [doSomething] 的方法。需要考虑两种情况:
- 方法 [Class02::doSomething] 由类外部调用。在此情况下,方法 [Class02::doSomething] 的优先级高于方法 [Trait01::doSomething],因此将调用前者;
- 方法 [Class02::doSomething] 由类内部调用。此时会发生冲突:解释器 PHP 无法确定应调用哪个方法;
第 19 行将方法 [Trait01::doSomething] 重命名为 [doSomethingInTrait]。因此,在 [Class02] 内部将使用以下写法:
- 使用 [doSomethingInTrait] 调用方法 [Trait01::doSomething];
- [doSomething] 用于调用方法 [Class02::doSomething];
- 第 25、27 行:类 [Class02] 将 [Trait01] 的属性和方法作为其自身的属性和方法使用;
- 第34-48行:类[Class03]与类[Class02]完全相同。 此处包含 [Trait01] 更为简单,因为 [Trait01] 和 [Class03] 的方法之间不存在冲突;
结果
需特别注意,[Trait01] 这一特征并未在 [Class02] 和 [Class03] 类之间共享。因此,[Trait01::i] 属性因 [Trait01] 被包含在 [Class02] 和 [Class03] 类中,而变成了两个不同的属性:[Class02::i] 和 [Class03::i]。结果的第2行和第4行显示了这一点。 如果 [Trait01::i] 属性被 [Class02] 和 [Class03] 这两个类共享,那么第 4 行显示的数值将是 20 而不是 10。
脚本 [trait-03.php] 表明,使用类代替特征也能得到相同的结果:
<?php
// 替换该特性的类
class Class01 {
// 属性
private $id = 0;
// 设置器
public function setId(int $id) {
$this->id = $id;
}
// getter
public function getId(): int {
return $this->id;
}
// 方法
public function doSomething(): void {
print "Class01::doSomething… ($this->id)\n";
}
}
class Class02 {
// 包含 Class01
private $class01;
// 设置器
public function setClass01(Class01 $class01) {
$this->class01 = $class01;
}
// 类专属方法
public function doSomething(): void {
// 修改 Class01 的属性
$id = $this->class01->getId();
$id += 10;
$this->class01->setId($id);
// 调用 Class01 的方法
$this->class01->doSomething();
// 本地显示
print "Class02->doSomething\n";
}
}
class Class03 {
// 包含 Class01
private $class01;
// 设置器
public function setClass01(Class01 $class01) {
$this->class01 = $class01;
}
// 类本地方法
public function doSomethingElse(): void {
// 修改 Class01 的属性
$id = $this->class01->getId();
$id += 10;
$this->class01->setId($id);
// 调用 Class01 的方法
$this->class01->doSomething();
// 本地显示
print "Class03->doSomethingElse\n";
}
}
// test01 ----------------
function test01(): void {
// 两个对象
$class02 = new Class02();
$class03 = new Class03();
// 将访问 [Class01] 的两个不同实例
$class02->setClass01(new Class01());
$class03->setClass01(new Class01());
// 验证
$class02->doSomething();
$class03->doSomethingElse();
}
// test02 ----------------
function test02(): void {
// [Class01] 的共享实例
$class01 = new Class01();
// 两个对象
$class02 = new Class02();
$class03 = new Class03();
// 将访问同一个 [Class01] 实例
$class02->setClass01($class01);
$class03->setClass01($class01);
// 验证
$class02->doSomething();
$class03->doSomethingElse();
}
// test01
print "test01-----------------\n";
test01();
// 测试02
print "test02-----------------\n";
test02();
注释
- 第 4-23 行:类 [Class01] 取代了属性 [Trait01]。 [Trait01] 的代码被整合到了类 [Class02] 和 [Class03] 的代码中。 在此情况下则不然。将引用类代码 [Class01],并将其注入类 [Class02] 和 [Class03] 中。 这意味着类 [Class01] 的属性将位于类 [Class02] 和 [Class03] 的代码之外。 由于此处 [id] 属性为私有(第 6 行),因此需要提供一个 getter(第 14-16 行)和一个 setter(第 9-11 行)。这是与特性的第一个区别:必须编写访问类中私有属性的代码。 虽然可以将属性 [id] 的可见性改为 [public],但绝不建议这样做。通过 setter 方法设置属性值可以验证其有效性;
- 第 27 行:在 [Class02] 的代码中引入对类 [Class01] 的引用。由于该属性为私有,我们需要创建一个 setter(第 30-32 行)来对其进行初始化;
- 第 37-41 行:在调用 [Class01] 代码时,必须通过 [$this→class01] 属性进行;
- 第48-69行:类[Class03]是类[Classe02]的克隆,只是其方法名称不同;
- 第72-82行:第一个测试。该测试通过向类[Class02]和[Class03]注入两个不同的[Class01]类实例(第77和78行)来实现;
- 第 85-97 行:第二个测试将 [Class01] 类的同一个实例注入到 [Class02] 和 [Class03] 类中(第 97、92、93 行);
- 第 100-101 行:执行测试 [test01];
- 第 103-104 行:执行测试 [test02];
结果
结果说明
- 第2-5行:结果与使用[Trait01]特性的情况相同。 由此可得出结论:此处使用特质并非必不可少,但它确实能减少代码量,因为无需额外方法来访问特质的属性——这些属性已是包含该特质的代码的组成部分;
- 第7-10行:由于在类[Class02]和[Class03]中注入了相同的[Class01]引用, [Class01::id] 属性被这两个类共享。这就是为什么结果的第 9 行显示 20 而不是 10(使用特性的情况)。由此可得出结论:如果特性的属性需要在类之间共享,那么该特性就无法使用,此时必须使用类;
9.4. 将方法归类到一个特质中
在前面的示例中,特质包含属性与方法。这里我们考虑特质仅包含方法的情况。在此情况下,特质类似于方法的抽象,可用于不同类中。由于该特质没有属性,我们将探讨其中聚合的方法仅处理传入参数的情况。 实际上,这并非强制要求:一个特质可以操作属性 [$this→attribut1],而自身并不拥有该属性。此时,需要使用该特质的类来提供属性 [$this→attribut1]。
若特质仅包含处理传入参数的方法,我们将证明该特质可被一个具有相同方法(且声明为静态)的类所替代。
脚本 [trait-04.php] 演示了该特性的使用,它沿用了前一个示例中的特性,但去除了所有属性:
<?php
trait Trait01 {
// 共享方法
public function doSomething() {
print "Trait01::doSomething ….\n";
}
}
class Class02 {
// 包含 Trait01
use Trait01 {
// 方法 [Trait01::doSomething] 已启用
// 在类中以名称 [doSomethingInTrait] 存在
Trait01::doSomething as doSomethingInTrait;
}
public function doSomething(): void {
// 调用Trait01方法
$this->doSomethingInTrait();
// 显示局部变量
print "Class02->doSomething\n";
}
}
class Class03 {
// 包含 Trait01
use Trait01;
// 类的方法
public function doSomethingElse(): void {
// 调用Trait01的方法
$this->doSomething();
// 本地显示
print "Class03->doSomethingElse\n";
}
}
// 测试 ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();
注释
- 第 3-10 行:[Trait01] 特质不再包含任何属性;
- 第 14-18 行:将 [Trait01] 包含在 [Class02] 中。第 22 行使用了 [Trait01] 的方法;
- 第 31 行:将 [Trait01] 包含在 [Class03] 中。第 36 行使用了 [Trait01] 的方法;
结果
在此用例中,[Trait01] 特性可轻松替换为类。如下面的 [trait-05.php] 脚本所示:
<?php
abstract class Class01 {
// 共享静态方法
public static function doSomething() {
print "Class01::doSomething ….\n";
}
}
class Class02 {
public function doSomething(): void {
// 调用 Class01 的方法
Class01::doSomething();
// 本地显示
print "Class02->doSomething\n";
}
}
class Class03 {
// 类本地方法
public function doSomethingElse(): void {
// 调用 Class01 的方法
Class01::doSomething();
// 本地显示
print "Class03->doSomethingElse\n";
}
}
// 测试 ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();
注释
- 第 3-10 行:将特征 [Trait01] 替换为抽象类 [Class01],该类的所有方法均声明为静态方法。该类仅被声明为抽象类,目的是防止其实例化。 原本也想编写 [final] 来阻止其派生,但 PHP 7 不接受类的前缀 [final abstract]。只能选其一,不能两者兼得;
- 第16行:不再写 [$this→doSomethingInTrait],而是写 [Class01::doSomething],即调用类 [Class01] 的静态方法 [doSomething];
- 第 28 行:在 [Class03] 中重复相同的操作;
结果
结果确实与使用特征 [Trait01] 时完全相同,这表明可以避免使用该特征。我们曾提到,特征的方法可以在不包含该特征的情况下,对属性 [$this→attribut1] 进行操作。 因此,应由使用该特性的类来提供 [$this→attribut1] 属性。这是一种特殊情况:不妨将 [$this→attribut1] 属性“上移”到特性本身中,因为使用该特性的类必须拥有该属性。这样,它就必然会成为使用该特性的类的属性之一。
9.5. 使用特性的多重继承
在 PHP 的相关文献中,常提到特质(trait)支持多重继承:即一个类可以继承自多个类。C++ 语言具备这一特性,但 Java 或 C# 语言仅支持单重继承。 我们将证明,虽然在派生类中使用特质确实可以实现类似多重继承的功能,但这种用例同样可以通过简单的类来实现。
脚本 [trait-06.php] 演示了派生类和特性的应用:
<?php
trait Trait01 {
// 属性
private $i;
// 共享方法
public function doSomethingInTrait01() {
// 修改 Trait01::$i
$this->i++;
// 显示
print "Trait01::doSomethingInTrait01… i=$this->i\n";
}
}
class Class02 {
// 属性
protected $j = 0;
// 方法
public function doSomethingInClass02(): void {
// 修改 Class02::j
$this->j += 10;
// 显示
print "Class02->doSomethingInClass02… j=$this->j\n";
}
}
// 派生类
class Class03 extends Class02 {
// 继承自 Class02:j 和 Trait01::i
// 包含 Trait01
use Trait01;
// 方法
public function doSomethingInClass03(): void {
// 使用 Trait01 的方法
$this->doSomethingInTrait01();
// 修改 Trait01::i
$this->i += 100;
// 修改 Class03::j (==Class02::j)
$this->j += 1000;
// 显示
print "Class03->doSomethingInClass03… i=$this->i, j=$this->j\n";
}
}
// 测试 ----------------
(new Class02())->doSomethingInClass02();
(new Class03())->doSomethingInClass03();
注释
- 第 3-15 行:回归到一个名为 [Trait01] 的特质,该特质包含一个属性及其操作方法;
- 第 17-29 行:一个名为 [Class02] 的类,与特征 [Trait01] 毫无关联。它并未使用该特征;
- 第 19 行:声明了 [Class02] 的唯一属性,并将其可见性设为 [protected],以便在派生类中访问该属性;
- 第 32 行:类 [Class03] 继承自类 [Class02]。此外,它还实现了特征 [Trait01](第 35 行)。 最后,它继承了 [Class02] 的属性和方法,并集成了 [Trait01] 的属性和方法。因此,这确实类似于多重继承;
结果
与前面的示例一样,我们将证明:
- 特质可以被类所替代;
- 与其将特性嵌入派生类中,不如嵌入该类的实例引用;
脚本 [trait-07.php] 如下:
<?php
class Class01 {
// 属性
protected $i;
// getter 和 setter
public function getI(): int {
return $this->i;
}
public function setI(int $i): void {
$this->i = $i;
}
// 方法
public function doSomethingInClass01(): void {
// 修改 Class01::$i
$this->i++;
// 显示
print "Class01::doSomething in Class01… i=$this->i\n";
}
}
class Class02 {
// 属性
protected $j = 0;
// 类专属方法
public function doSomethingInClass02(): void {
// 修改 Class02::j
$this->j += 10;
// 显示
print "Class02->doSomethingInClass02… j=$this->j\n";
}
}
class Class03 extends Class02 {
// 包含 Class01
private $class01;
// 设置器
public function setClass01(Class01 $class01) {
$this->class01 = $class01;
}
// 类局部方法
public function doSomethingInClass03(): void {
// 调用 Class01 的方法
$this->class01->doSomethingInClass01();
// 修改 Class01::i
$i = $this->class01->getI();
$i += 100;
$this->class01->setI($i);
// 修改 Class03::j
$this->j += 1000;
// 显示
print "Class03->doSomethingInClass03… i=$i, j=$this->j\n";
}
}
// 测试 ----------------
$class01 = new Class01();
$class02 = new Class02();
$class03 = new Class03();
$class03->setClass01($class01);
$class02->doSomethingInClass02();
$class03->doSomethingInClass03();
注释
- 第 3-24 行:类 [Class01] 取代了特性 [Trait01]。 由于类 [Class01] 将通过引用被纳入其他类中,因此为属性 [$i] 提供了 get / set 方法;
- 第26-38行:类[Classe02]保持不变;
- 第 40 行:类 [Class03] 继承自类 [Classe02];
- 第 42 行:类 [Classe01] 通过引用被包含在 [Classe03] 中;
- 第45-47行:定义类setter,用于初始化对类[Classe01]的引用;
- 第50-61行:方法[doSomethingInClass03]与前文功能相同,但代码更为复杂;
结果
从这个示例中可以得出结论:特质(trait)在此处并非必不可少,但必须承认它能让派生类中的代码写得更简洁。
9.6. 使用特质代替抽象类
我们经常遇到以下使用场景:创建一个相当通用的接口 I,它可以衍生出多个实现。这些实现共享共同的代码,但在其他方法上有所不同。可以通过两种方式实现这种使用场景:
- 我们创建一个抽象类 C,用于封装派生类共有的代码。类 C 实现了接口 I,但其中某些需要在派生类中声明的方法在类 C 中被声明为抽象方法,因此类 C 本身也是抽象类。 随后创建从 C 派生的类 C1 和 C2,它们各自以自己的方式实现了父类 C 中未定义(抽象)的方法;
- 创建一个与前一解决方案中的抽象类 C 几乎完全相同的特征 T。该特征不实现接口 I,因为从语法上讲它无法实现。 随后创建类 C1 和 C2,它们实现接口 I 并使用特征 T。这些类只需实现特征 T 未实现的接口 I 中的方法即可;
以下示例展示了这两种解决方案的相似性。
应用程序 1 实现了前面描述的解决方案 1 [trait-08.php]:
<?php
interface Interface1 {
public function doSomething(): void;
public function doSomethingElse(): void;
}
abstract class AbstractClass implements Interface1 {
// 属性
protected $attr1 = 11;
protected $attr2 = 12;
// 获取器和设置器
public function getAttr1() {
return $this->attr1;
}
public function getAttr2() {
return $this->attr2;
}
public function setAttr1($attr1) {
$this->attr1 = $attr1;
return $this;
}
public function setAttr2($attr2) {
$this->attr2 = $attr2;
return $this;
}
// 已实现的方法
public function doSomething(): void {
print "AbstractClass::doSomething [$this->attr1,$this->attr2]\n";
}
// 未实现的方法
abstract public function doSomethingElse(): void;
}
// 派生类 1
class Class1 extends AbstractClass {
// 属性
private $attr3 = 13;
// 获取器和设置器
public function getAttr3() {
return $this->attr3;
}
public function setAttr3($attr3) {
$this->attr3 = $attr3;
return $this;
}
// 实现 doSomethingElse
public function doSomethingElse(): void {
print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
}
}
// 派生类 2
class Class2 extends AbstractClass {
// 属性
private $attr4 = 14;
public function getAttr4() {
return $this->attr4;
}
public function setAttr4($attr4) {
$this->attr4 = $attr4;
return $this;
}
// 实现 doSomethingElse
public function doSomethingElse(): void {
print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
}
}
// 外部函数
function useInterfaceWith(Interface1 $interface):void{
$interface->doSomething();
$interface->doSomethingElse();
}
// 测试
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());
注释
- 第 3-8 行:接口 [Interface1] 有两个方法;
- 第 10-41 行:抽象类 [AbstractClass] 实现了接口 [Interface1](第 10 行)。 它拥有两个属性及其相应的获取器和设置器(第12-32行),实现了接口[Interface1]中的方法[doSomething](第35-37行) 但无法实现方法 [doSomethingElse]。因此该方法被声明为抽象方法(第 40 行)。抽象类 [AbstractClass] 无法被实例化,若要发挥作用,必须通过派生来实现;
- 第44-63行:Class1类继承了抽象类[AbstractClass],因此实现了接口[Interface1](第14行)。 它为其父类未定义的方法 [doSomethingElse] 提供了具体实现(第 59-61 行)。它还为其父类的属性添加了新属性(第 46-56 行);
- 第 66-82 行:Class2 类继承了抽象类 [AbstractClass],因此实现了接口 [Interface1](第 66 行)。 它为父类未定义的方法 [doSomethingElse] 提供了实现(第 80-82 行)。它还向父类的属性中添加了一个属性(第 68-77 行);
- 第 87-90 行:函数 [useInterfaceWith] 接收类型 [Interface1] 作为参数,并调用该接口的两个方法;
- 第 93-94 行:第一次调用函数 [useInterfaceWith] 时传入类型 [Class1],第二次调用时传入类型 [Class2]。 这是正确的,因为这两个类型都实现了 [Interface1] 接口;
结果
AbstractClass::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
AbstractClass::doSomething [11,12]
Class2::doSomethingElse [11,12,14]
现在我们使用脚本 [trait-09.php] 实现方案 2。这涉及将抽象类替换为特质:
<?php
interface Interface1 {
public function doSomething(): void;
public function doSomethingElse(): void;
}
trait Trait1 {
// 属性
private $attr1 = 11;
private $attr2 = 12;
// 获取器和设置器
public function getAttr1() {
return $this->attr1;
}
public function getAttr2() {
return $this->attr2;
}
public function setAttr1($attr1) {
$this->attr1 = $attr1;
return $this;
}
public function setAttr2($attr2) {
$this->attr2 = $attr2;
return $this;
}
// 已实现的方法
public function doSomething(): void {
print "Trait::doSomething [$this->attr1,$this->attr2]\n";
}
}
// 派生类 1
class Class1 implements Interface1 {
// 特性的使用
use Trait1;
// 属性
private $attr3 = 13;
// 获取器和设置器
public function getAttr3() {
return $this->attr3;
}
public function setAttr3($attr3) {
$this->attr3 = $attr3;
return $this;
}
// 实现doSomethingElse
public function doSomethingElse(): void {
print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
}
}
// 派生类 2
class Class2 implements Interface1 {
// 特性的使用
use Trait1;
// 属性
private $attr4 = 14;
public function getAttr4() {
return $this->attr4;
}
public function setAttr4($attr4) {
$this->attr4 = $attr4;
return $this;
}
// 实现 doSomethingElse
public function doSomethingElse(): void {
print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
}
}
// 使用接口的外部函数
function useInterfaceWith(Interface1 $interface): void {
$interface->doSomething();
$interface->doSomethingElse();
}
// 测试
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());
注释
- 第 3-8 行:接口 [Interface1] 未作更改;
- 第10-41行:特征[Trait1]取代了方案1中的抽象类[AbstractClass]。代码基本相同,仅存在以下细微差异:
- 第 10 行:[Trait1] 特质未实现 [Interface1] 接口。这在语法上是不可能的;
- 第 12-13 行:抽象类 [AbstractClass] 的属性可见性 [protected] 在此处变为 [private]。 这两个属性旨在让派生类能够直接访问父类(protected)或特性的(private)属性,而无需通过getter和setter方法;
- 特征 [Trait1] 未声明抽象方法 [doSomethingElse];
- 第41-62行:方案2中的类[Class1]与方案1中的类[Class1]完全相同,仅存在以下细微差异:
- 第 41 行:类 [Class1] 实现了接口 [Interface1],而在方案 1 中,它继承了抽象类 [AbstractClass];
- 第 43 行:它使用特征 [Trait1] 来实现接口的一部分;
- 第 65-85 行:可参照 [Class1] 的说明;
- 第87-95行:其余代码保持不变;
结果
Trait::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
Trait::doSomething [11,12]
Class2::doSomethingElse [11,12,14]
确实得到了相同的结果。
9.7. 结论
从前面的示例可以看出,使用破折号能带来净收益的使用场景并不明确。在我们的示例中,始终可以通过将其替换为类来省略它。然而,在将代码在不同的派生类之间进行提取时,使用破折号似乎很实用,仿佛该代码属于一个父类一样。这就是我们在接下来的示例中将要做的事情。