PHP 中 Trait 详解及其应用

从PHP的5.4.0版本开始,PHP提供了一种全新的代码复用的概念,那就是Trait。Trait其字面意思是”特性”、”特点”,我们可以理解为,使用Trait关键字,可以为PHP中的类添加新的特性。

Trait

自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。

Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait
为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用
method。Trait 和 Class
组合的语义定义了一种减少复杂性的方式,避免传统多继承和 Mixin
类相关典型问题。

Trait 和 Class 相似,但仅仅旨在用细粒度和一致的方式来组合功能。 无法通过
trait
自身来实例化。它为传统继承增加了水平特性的组合;也就是说,应用的几个
Class 之间不需要继承。

Example #1 Trait 示例

<?phptrait ezcReflectionReturnInfo {    function getReturnType() { /*1*/ }    function getReturnDescription() { /*2*/ }}class ezcReflectionMethod extends ReflectionMethod {    use ezcReflectionReturnInfo;/* ... */}class ezcReflectionFunction extends ReflectionFunction {    use ezcReflectionReturnInfo;/* ... */}?>

熟悉面向对象的都知道,软件开发中常用的代码复用有继承和多态两种方式。在PHP中,只能实现单继承。而Trait则避免了这点。下面通过简单的额例子来进行对比说明。

优先级

从基类继承的成员会被 trait
插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 trait 的方法,而
trait 则覆盖了被继承的方法。

Example #澳门新浦京电子游戏,2 优先顺序示例

从基类继承的成员被插入的 SayWorld Trait 中的 MyHelloWorld
方法所覆盖。其行为 MyHelloWorld
类中定义的方法一致。优先顺序是当前类中的方法会覆盖 trait 方法,而 trait
方法又覆盖了基类中的方法。

<?phpclass Base {    public function sayHello() {        echo 'Hello ';    }}trait SayWorld {    public function sayHello() {parent::sayHello();        echo 'World!';    }}class MyHelloWorld extends Base {    use SayWorld;}$o = new MyHelloWorld();$o->sayHello();?>

以上例程会输出:

Hello World!

Example #3 另一个优先级顺序的例子

<?phptrait HelloWorld {    public function sayHello() {        echo 'Hello World!';    }}class TheWorldIsNotEnough {    use HelloWorld;    public function sayHello() {        echo 'Hello Universe!';    }}$o = new TheWorldIsNotEnough();$o->sayHello();?>

以上例程会输出:

Hello Universe!

1. 继承 VS 多态 VS Trait

现在有Publish.phpAnswer.php这两个类。要在其中添加LOG功能,记录类内部的动作。有以下几种方案:

  • 继承
  • 多态
  • Trait
多个 trait

通过逗号分隔,在 use 声明列出多个 trait,可以都插入到一个类中。

Example #4 多个 trait 的用法

<?phptrait Hello {    public function sayHello() {        echo 'Hello ';    }}trait World {    public function sayWorld() {        echo 'World';    }}class MyHelloWorld {    use Hello, World;    public function sayExclamationMark() {        echo '!';    }}$o = new MyHelloWorld();$o->sayHello();$o->sayWorld();$o->sayExclamationMark();?>

以上例程会输出:

Hello World!

1.1. 继承

如图:

澳门新浦京电子游戏 1

代码结构如下:

// Log.php
<?php
Class Log
{
    public function startLog()
    {
        // echo ...
    }

    public function endLog()
    {
        // echo ...
    }
}

// Publish.php
<?php
Class Publish extends Log
{

}

// Answer.php
<?php
Class Answer extends Log
{

}

可以看到继承的确满足了要求。但这却违背了面向对象的原则。而发布(Publish)和回答(Answer)这样的操作和日志(Log)之间的关系并不是子类与父类的关系。所以不推荐这样使用。

冲突的解决

如果两个 trait
都插入了一个同名的方法,如果没有明确解决冲突将会产生一个致命错误。

为了解决多个 trait 在同一个类中的命名冲突,需要使用 insteadof
操作符来明确指定使用冲突方法中的哪一个。

以上方式仅允许排除掉其它方法,as
操作符可以将其中一个冲突的方法以另一个名称来引入。

Example #5 冲突的解决

在本例中 Talker 使用了 trait A 和 B。由于 A 和 B
有冲突的方法,其定义了使用 trait B 中的 smallTalk 以及 trait A 中的
bigTalk。

Aliased_Talker 使用了 as 操作符来定义了 talk 来作为 B 的 bigTalk
的别名。

<?phptrait A {    public function smallTalk() {        echo 'a';    }    public function bigTalk() {        echo 'A';    }}trait B {    public function smallTalk() {        echo 'b';    }    public function bigTalk() {        echo 'B';    }}class Talker {    use A, B {B::smallTalk insteadof A;A::bigTalk insteadof B;    }}class Aliased_Talker {    use A, B {B::smallTalk insteadof A;A::bigTalk insteadof B;B::bigTalk as talk;    }}?>

1.2. 多态

如图:

澳门新浦京电子游戏 2

实现代码:

// Log.php
<?php
Interface Log
{
    public function startLog();
    public function endLog();
}

// Publish.php
<?php
Class Publish implements Log
{
    public function startLog()
    {
        // TODO: Implement startLog() method.
    }
    public function endLog()
    {
        // TODO: Implement endLog() method.
    }
}

// Answer.php
<?php
Class Answer implements Log
{
    public function startLog()
    {
        // TODO: Implement startLog() method.
    }
    public function endLog()
    {
        // TODO: Implement endLog() method.
    }
}

记录日志的操作应该都是一样的,因此,发布(Publish)和回答(Answer)动作中的日志记录实现也是一样的。很明显,这违背了DRY(Don’t
Repeat Yourself)原则。所以是不推荐这样实现的。

修改方法的访问控制

使用 as 语法还可以用来调整方法的访问控制。

Example #6 修改方法的访问控制

<?phptrait HelloWorld {    public function sayHello() {        echo 'Hello World!';    }}// 修改 sayHello 的访问控制class MyClass1 {    use HelloWorld { sayHello as protected; }}// 给方法一个改变了访问控制的别名// 原版 sayHello 的访问控制则没有发生变化class MyClass2 {    use HelloWorld { sayHello as private myPrivateHello; }}?>

1.3. Trait

如图:

澳门新浦京电子游戏 3

实现代码如下:

// Log.php
<?php
trait Log{
    public function startLog() {
        // echo ..
    }
    public function endLog() {
        // echo ..
    }
}

// Publish.php
<?php
class Publish {
    use Log;
}
$publish = new Publish();
$publish->startLog();
$publish->endLog();

// Answer.php
<?php
class Answer {
    use Log;
}
$answer = new Answer();
$answer->startLog();
$answer->endLog();

可以看到,我们在没有增加代码复杂的情况下,实现了代码的复用。

从 trait 来组成 trait

正如 class 能够使用 trait 一样,其它 trait 也能够使用 trait。在 trait
定义时通过使用一个或多个 trait,能够组合其它 trait 中的部分或全部成员。

Example #7 从 trait 来组成 trait

<?phptrait Hello {    public function sayHello() {        echo 'Hello ';    }}trait World {    public function sayWorld() {        echo 'World!';    }}trait HelloWorld {    use Hello, World;}class MyHelloWorld {    use HelloWorld;}$o = new MyHelloWorld();$o->sayHello();$o->sayWorld();?>

以上例程会输出:

Hello World!

1.4. 结论

继承的方式虽然也能解决问题,但其思路违背了面向对象的原则,显得很粗暴;多态方式也可行,但不符合软件开发中的DRY原则,增加了维护成本。而Trait方式则避免了上述的不足之处,相对优雅的实现了代码的复用。

Trait 的抽象成员

为了对使用的类施加强制要求,trait 支持抽象方法的使用。

Example #8 表示通过抽象方法来进行强制要求

<?phptrait Hello {    public function sayHelloWorld() {        echo 'Hello'.$this->getWorld();    }    abstract public function getWorld();}class MyHelloWorld {    private $world;    use Hello;    public function getWorld() {        return $this->world;    }    public function setWorld($val) {$this->world = $val;    }}?>

2. Trait的作用域

了解了Trait的好处,我们还需要了解其实现中的规则,先来说一下作用域。这个比较好证明,实现代码如下:

<?php
class Publish {
    use Log;
    public function doPublish() {
        $this->publicF();
        $this->protectF();
        $this->privateF();
    }
}
$publish  = new Publish();
$publish->doPublish();

执行上述代码输出结果如下:

public function
protected function
private function

可以发现,Trait的作用域在引用该Trait类的内部是都可见的。可以理解为use关键字将Trait的实现代码Copy了一份到引用该Trait的类中。

Trait 的静态成员

Traits 可以被静态成员静态方法定义。

Example #9 静态变量

<?phptrait Counter {    public function inc() {        static $c = 0;$c = $c + 1;        echo "$cn";    }}class C1 {    use Counter;}class C2 {    use Counter;}$o = new C1(); $o->inc(); // echo 1$p = new C2(); $p->inc(); // echo 1?>

Example #10 静态方法

<?phptrait StaticExample {    public static function doSomething() {        return 'Doing something';    }}class Example {    use StaticExample;}Example::doSomething();?>

3. Trait中属性的优先级

说到优先级,就必须要有一个对比的参照物,这里的参照对象时引用Trait的类及其父类。

通过以下的代码来证明Trait应用中的属性的优先级:

<?php
trait Log
{
    public function publicF()
    {
        echo __METHOD__ . ' public function' . PHP_EOL;
    }
    protected function protectF()
    {
        echo __METHOD__ . ' protected function' . PHP_EOL;
    }
}

class Question
{
    public function publicF()
    {
        echo __METHOD__ . ' public function' . PHP_EOL;
    }
    protected function protectF()
    {
        echo __METHOD__ . ' protected function' . PHP_EOL;
    }
}

class Publish extends Question
{
    use Log;

    public function publicF()
    {
        echo __METHOD__ . ' public function' . PHP_EOL;
    }
    public function doPublish()
    {
        $this->publicF();
        $this->protectF();
    }
}
$publish = new Publish();
$publish->doPublish();

上述代码的输出结果如下:

Publish::publicF public function
Log::protectF protected function

通过上面的例子,可以总结出Trait应用中的优先级如下:

  1. 来自当前类的成员覆盖了 trait 的方法
  2. trait 覆盖了被继承的方法

类成员优先级为:当前类>Trait>父类

属性

Trait 同样可以定义属性。

Example #11 定义属性

<?phptrait PropertiesTrait {    public $x = 1;}class PropertiesExample {    use PropertiesTrait;}$example = new PropertiesExample;$example->x;?>

如果 trait
定义了一个属性,那类将不能定义同样名称的属性,否则会产生一个错误。如果该属性在类中的定义与在
trait 中的定义兼容(同样的可见性和初始值)则错误的级别是
E_STRICT,否则是一个致命错误。

Example #12 解决冲突

<?phptrait PropertiesTrait {    public $same = true;    public $different = false;}class PropertiesExample {    use PropertiesTrait;    public $same = true; // Strict Standardspublic $different = true; // 致命错误}?>

4. Insteadof和As关键字

在一个类中,可以引用多个Trait,如下:

<?php
trait Log
{
    public function startLog()
    {
        echo __METHOD__ . ' public function' . PHP_EOL;
    }
    protected function endLog()
    {
        echo __METHOD__ . ' protected function' . PHP_EOL;
    }
}

trait Check
{
    public function parameterCheck($parameters) {
        // do sth
    }
}

class Publish extends Question
{
    use Log,Check;
    public function doPublish($para) {
        $this->startLog();
        $this->parameterCheck($para);
        $this->endLog();
    }
}

通过上面的方式,我们可以在一个类中引用多个Trait。引用多个Trait的时候,就容易出问题了,最常见的问题就是两个Trait中如果出现了同名的属性或者方法该怎么办呢?这个时候就需要用到Insteadof 和 as 这两个关键字了.请看如下实现代码:

<?php

trait Log
{
    public function parameterCheck($parameters)
    {
        echo __METHOD__ . ' parameter check' . $parameters . PHP_EOL;
    }

    public function startLog()
    {
        echo __METHOD__ . ' public function' . PHP_EOL;
    }
}

trait Check
{
    public function parameterCheck($parameters)
    {
        echo __METHOD__ . ' parameter check' . $parameters . PHP_EOL;
    }

    public function startLog()
    {
        echo __METHOD__ . ' public function' . PHP_EOL;
    }
}

class Publish
{
    use Check, Log {
        Check::parameterCheck insteadof Log;
        Log::startLog insteadof Check;
        Check::startLog as csl;
    }

    public function doPublish()
    {
        $this->startLog();
        $this->parameterCheck('params');
        $this->csl();
    }
}

$publish = new Publish();
$publish->doPublish();

执行上述代码,输出结果如下:

Log::startLog public function
Check::parameterCheck parameter checkparams
Check::startLog public function

就如字面意思一般,insteadof关键字用前者取代了后者,as 关键字给被取代的方法起了一个别名。

在引用Trait时,使用了use关键字,use关键字也用来引用命名空间。两者的区别在于,引用Trait时是在class内部使用的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注