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


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


1.为什么要用trait

假如我们现在有两个类:order和user,我们希望这两个类都能提供记录日志的功能,那我们有几种实现方式呢,我想到的只有下面几种:

使用extends

使用interface

使用trait

  • 使用extends

class Log{
    public function WriteLog(){
        
    }
}

class Order extends Log{
    
}

class User extends Log{
    
}

这种方法在语法上是可行的,但是仔细想想总感觉这样不舒服,我觉得继承并不是这样用的,继承是分层次的,下一层的分类是上一层的特化,也继承了上一层的所有特性。但是在这儿Order和User根本就和Log没有任何关系,也就是Order和User根本就不是一种Log类,所以说,在这种场景下用继承我觉得是错误的用法。

  • 使用interface

interface ILog{
    public function WriteLog();
}

class Order implements  Log{
    public function WriteLog() 
    {
    }
}

class User implements  Log{
    public function WriteLog() 
    {
    }
}

现在看用接口的方式比继承好多了,不过仔细想想,感觉还是不好,因为Order和User类中需要实现WriteLog的方法,这导致出现一个问题,以后万一WriteLog出现了Bug,那么Order和User都需要修改WriteLog的代码,这明显的违反了DRY原则(Don’t Repeat Yourself),所以这种方式也不是最优方案。

  • 使用trait

其实trait就是为这类问题而生(我也是刚学,姑且这么认为吧),trait允许你写一个半Class的Log trait,然后将Log trait注入到Order和User中。

trait Log{
    public function WriteLog(){
        ...
    }
}

class Order{
   Use Log;
}

class User{
   Use Log;
}

因为trait允许我们把实现代码写在trait中的,这样我们就可以避免用接口实现带来的代码重复的问题。

既然trait已经将WriteLog都写好了,其他类就不用实现了,PHP使用了Use关键字,让我们将trait的函数直接注入到Order类与User类。也就是说, 虽然Order与User都没有实现WriteLog,但是通过使用注入Log trait之后,就相当于有了WiteLog方法了。

对于PHP来说,在使用Use关键字时,PHP只是将trait的所有变量与方法“复制”进Class内,让class马上拥有Trait的所有功能。

2.trait的作用域

因为trait在实作上是使用复制, 所以原本在trait内申明的public、protected、private变量与方法都会复制到class内,也就是说,class内被trait加进来变量与方法的作用域将与原trait完全一样。

比较一下继承的extends,当使用继承时,只有public与protected的变数与函式会被继承下来,private函式将不会被继承下来。

3.insteadof和as关键字

由于PHP使用“复制”的办法来把trait引入到类中来,所以有可能出现trait所定义的变量与方法已经在其他class已经被定义,也就是变量与方法可能同名冲突,为了解决这种冲突,PHP给出了insteadof与as关键字来解决。

trait 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;//通过insteadof关键字告诉PHP我们要用B trait中的smallTalk,而不要使用A trait中的smallTalk
        A::bigTalk insteadof B;
    }
}

class Aliased_Talker {
    use A, B {
        B::smallTalk insteadof A;
        A::bigTalk insteadof B;
        B::bigTalk as talk; //通过 as 关键字告诉PHP我们在随后的代码中我们要用talk来替代B::bigTalk
    }
}

可能有人会问如果我要用A::smallTalk怎么办,其实很简单的,直接用as关键字就可以了,A::smallTalk as AsmallTalk。

如果我在解决冲突时直接用as而不用insteadof是否可以,不可以的,必须先用insteadof来告诉PHP冲突时使用哪个trait中的方法,如果你想用被排除的方法时就需要用as关键字了


4.优先级

1)同名方法优先级

优先级从大到小依次是这样的:

当前类-->trait-->父类

咱们通过代码来说明 

class Base {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait SayWorld {
    public function sayHello() {
        echo 'World!';
    }
}

class MyHelloWorld extends Base {
    use SayWorld;
    public function sayHello() {
        echo 'PHP!';
    }
}

$o = new MyHelloWorld();
$o->sayHello(); //此处输出的是 PHP!

咱们再修改下代码

class Base {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait SayWorld {
    public function sayHello() {
        echo 'World!';
    }
}

class MyHelloWorld extends Base {
    use SayWorld;
}

$o = new MyHelloWorld();
$o->sayHello(); //此处输出的是World!

2)变量优先级

Trait 定义了一个属性后,类就不能定义同样名称的属性,否则会产生 fatal error。 有种情况例外:属性是兼容的(同样的访问可见度、初始默认值)。 在 PHP 7.0 之前,属性是兼容的,则会有 E_STRICT 的提醒。

class Base {
    public $a='World!';
}

trait SayWorld {
    public $a='PHP!';
}

class MyHelloWorld extends Base {
    use SayWorld;
}

$o = new MyHelloWorld();
echo $o->a;
这个代码会产生一个Fatal错误:PHP Fatal error:  Base and SayWorld define the same property ($a) in the composition of MyHelloWorld. However, the definition differs and is considered incompatible.
class Base {
    public $a='World!';
}

trait SayWorld {
    public $a='World!';
}

class MyHelloWorld extends Base {
    use SayWorld;
}

$o = new MyHelloWorld();
echo $o->a;
这个代码正常输出:World!

5.修改方法的访问控制

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

trait HelloWorld {
    public function sayHello() {
        echo 'Hello World!';
    }
}

// 修改 sayHello 的访问控制
class MyClass1 {
    use HelloWorld { sayHello as protected; }
}

// 给方法一个改变了访问控制的别名
// 原版 sayHello 的访问控制则没有发生变化
class MyClass2 {
    use HelloWorld { sayHello as private myPrivateHello; }
}

6.Trait的抽象成员

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

trait 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;
    }
}

7.Trait的静态成员

trait Counter {
    public function inc() {
        static $c = 0;
        $c = $c + 1;
        echo "$c\n";
    }
}

class C1 {
    use Counter;
}

class C2 {
    use Counter;
}

$o = new C1(); $o->inc(); // echo 1
$p = new C2(); $p->inc(); // echo 1

8.Trait的静态方法

trait StaticExample {
    public static function doSomething() {
        return 'Doing something';
    }
}

class Example {
    use StaticExample;
}

Example::doSomething();

9.Trait的常量

在Trait中不允许定义常量的

5.最佳实践