一、什么是Trait
我们知道PHP类是不能多继承的,Trait就是类似于多继承的一种代码复用机制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。
实际上,这里说Trait是多继承不太恰当,虽然它能达到多继承的效果,复用多个类的方法,但是无法描述多继承中的对象归属关系(即is-a关系)。更确切的来说,trait提供的是一种方法的组合功能,类似于接口类,能反映类与方法的包含关系(has-a关系),只是接口类无法实现方法,而trait可以。
接下来我们通过一个场景来看看trait如何使用,能带来什么好处。
请看如下场景:如果有若干个类需要使用单例模式,为了复用实例化对象相关的方法代码,我们能想到的做法是在父类实现单例功能,子类继承并实现具体的其他功能。
然而事情没有那么简单,假如现在业务上有3个需求,我们需要写3个方法(简称为方法1~3)来分别满足这3个需求。并且上述的子类A需要使用方法1和3,子类B使用方法1和2,子类C使用方法2和3,而且这三个方法业务上毫无关联(也就是分属3个不同业务的需求),因此将三个方法放到父类中实现虽然满足复用性,却在逻辑上不合理,此时就需要Trait解决我们的痛点。
我们可以将 方法1~3 分别放到 Trait1~3,然后子类1使用Trait1和Trait3,子类2使用 Trait1 和 Trait2。也就是说一个业务类可以随意的组合使用哪几个 Trait,从而让这个业务类具备哪些几个功能。
下面看个例子:
trait Singleton
{
private static $instance;
public static function getInstance() {
if (!(self::$instance instanceof self)) {
self::$instance = new self;
}
return self::$instance;
}
}
class DbReader extends ArrayObject
{
use Singleton;
}
class FileReader
{
use Singleton;
}
Trait 与抽象类类似,可定义抽象方法和正常方法,但自身无法实例化。通过在一个类中 use 一个Trait,该Trait内的方法和属性就会被水平的注入该类中。
所谓的水平的注入,意思是如果一个Trait定义了某些属性或方法,那么use这个Trait的类A也可以使用这些属性/方法,即便这些属性/方法是私有的,因为Trait中的所有属性和方法都被注入到了类A,这些属性和方法就变成了类A的属性和方法。而对于继承而言,子类是不能使用父类的私有属性/方法的。因此这一点也体现了Trait特性只能说表面类似于多继承,实际上不算是多继承,而是一种组合模式。
二、Trait特性
为了方便说明,下面内容中我们将 use Trait 语句所在的类称为使用类
0、Trait自身无法实例化,而且Trait的静态方法或属性不能直接由Trait自身调用,只能由其使用类调用。
1、私有属性/方法水平注入,被注入trait的类(即使用类)可访问trait的私有成员,反之trait也可以访问被注入trait的类的私有成员,当然后者的情况很少,因为一般来说trait本身是不知道它的实现类中有些什么属性。
例如:
<?php
trait Message
{
function alert() {
echo $this->message;
}
}
class Messenger
{
use Message;
private $message = "This is a message";
}
2、一个类可以use多个trait,一个trait可以也可以use多个trait组合成一个新trait。
<?php
trait Hello
{
function sayHello() {
echo "Hello";
}
}
trait World
{
function sayWorld() {
echo "World";
}
}
trait HelloWorld
{
use Hello, World;
}
class MyWorld
{
use HelloWorld;
}
$world = new MyWorld();
echo $world->sayHello() . " " . $world->sayWorld(); //Hello World
3、对于方法而言:trait 的方法覆盖从父类继承的方法(不难理解,因为trait和被注入trait的类是同级的),当前类定义的方法覆盖 trait 的方法。
例如:
<?php
trait Hello
{
function sayHello() {
return "Hello";
}
function sayWorld() {
return "Trait World";
}
function sayHelloWorld() {
echo $this->sayHello() . " " . $this->sayWorld();
}
function sayBaseWorld() {
echo $this->sayHello() . " " . parent::sayWorld();
}
}
class Base
{
function sayWorld(){
return "Base World";
}
}
class HelloWorld extends Base
{
use Hello;
function sayWorld() {
return "World";
}
}
$h = new HelloWorld();
$h->sayHelloWorld(); // Hello World
$h->sayBaseWorld(); // Hello Base World
对于非静态属性而言:如果一个 trait 定义了一个属性,那么一个使用类不能定义一个具有相同名称的属性,除非它具有相同的可见性和初始值,否则会发出致命错误。
<?php
trait PropertiesTrait {
public $same = true;
public $different = false;
}
class PropertiesExample {
use PropertiesTrait;
public $same = true;
public $different = true; // Fatal error
}
?>
对于静态属性而言:对于trait中的某个静态属性,多个使用类的该静态属性是相互独立的(而对于继承而言,父类中的静态属性在所有子类中是共享的,一个子类对其修改,另一个子类访问到的是修改后的值)。
<?php
trait Singleton
{
protected static $name;
public static function setName($name){
self::$name = $name;
}
public static function getName(){
return self::$name;
}
}
class DbReader
{
use Singleton;
}
class FileReader
{
use Singleton;
}
DbReader::setName('zbp');
FileReader::setName('abc');
var_dump(DbReader::getName()); // zbp
var_dump(FileReader::getName()); // abc
var_dump(Singleton::getName()); // 报Deprecated并返回null
4、多个 traits 使用了相同的方法名将抛出一个致命错误,需要insteadof关键字解决冲突。
<?php
trait Game
{
function play() {
echo "Playing a game";
}
}
trait Music
{
function play() {
echo "Playing music";
}
}
class Player
{
use Game, Music {
Music::play insteadof Game;
}
}
$player = new Player();
$player->play(); //玩音乐
$player = new Player();
$player->play();
另一种方式是保留两个方法,为其中给一个方法起别名。
<?php
class Player
{
use Game, Music {
Game::play as gamePlay;
Music::play insteadof Game;
}
}
$player = new Player();
$player->play(); //玩音乐
$player->gamePlay(); //玩游戏
5、trait可以定义抽象方法来强制要求使用类必须实现这些抽象方法。
6、PHP 的 Trait 可以拥有构造器(__construct(),如果使用类也定义了构造方法,则实用类的构造方法会覆盖Trait的构造方法,且无法通过parent::__construct的方式保留),但是必须将其声明为 public,如果声明 protected 或 private 将会报错。在 Trait 中使用构造器时应当谨慎,因为经常会导致使用类出现意料之外的冲突。
7、可以使用as关键字改变trait中的方法的可见性(可以在使用类中将trait的方法的权限改大或者改小)。
<?php
trait HelloWorld {
public function sayHello() {
echo 'Hello World!';
}
}
class MyClass1 {
use HelloWorld { sayHello as protected; }
}
class MyClass2 {
use HelloWorld { sayHello as private myPrivateHello; }
}
?>