PHP7语言基础——类与对象

面向对象编程

面向对象编程(Object Oriented Programming, OOP)是一种被很多语言广泛支持的编程模式,面向对象编程的思想是把具有相似特性的事物抽象成类,通过类的属性和方法的定义实现代码共用。

将实现某一特定功能的代码部分进行封装,使得封装代码可被多出调用,封装的粒度越小被重用的概率越大。

面向对象编程的继承性和多态性也提高了代码的复用度。面向对象编程充分体现了“高内聚,低耦合”的思想。

对象(Object)在OOP中是由属性和操作组成的。属性(attributes)就是对象的特性或对象关联的惯量。操作(operation)就是对象中的方法(method)或者函数(function)。

OOP三大特点:

  1. 封装性:将类的使用和实现分开管理,只保留类的接口,也就是说使用对象不需要知道类的实现过程,只需要知道如何使用即可。
  2. 继承性:“继承”是OOP软件技术中的一个概念。如果一个类A继承自另一个类B,就把这个A称为“B的子类”,而把B称为“A的父类”。继承可以使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码。在令子类继承父类的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类的原有属性和方法,使其获得与父类不同的功能。继承可以实现代码的可重用性,简化对象和类的创建过程。PHP支持单继承,也就是一个子类只能有一个父类。
  3. 多态性:同一操作作用于不同的类的实例,将产生不同的执行结果,即不同类的对象收到相同的消息时,得到不同的结果。

类是对象的抽象,对象是类的实例。类就像是一个生产对象的工厂,而对象就是这个工厂生产出来的具体产品。一个工厂生产出来的产品都有一些共同的特点,好比一个生产汽车的工厂,生产出的汽车都有轮子、方向盘、座椅等,当然汽车都能行驶、载人。在编程中,轮子、方向盘、座椅就是类的属性,行驶、载人就是类的方法。

类的声明

每个类的定义都以关键字class开头,后面跟着类名,后面跟着一对花括号,里面包含有类的属性与方法的定义。

类名可以是任何非 PHP保留字的合法标签。一个合法类名以字母或下划线开头,后面跟着若干字母,数字或下划线。以正则表达式表示为:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]

一个类可以包含有属于自己的常量,变量(称为"属性")以及函数(称为"方法")。

【示例】

<?php
class SimpleClass {
	// 声明属性
	public $var = 'a default value';
	
	// 声明方法
	public function displayVar() {
		echo $this->var;
	}
}
?>

其中类似pulbic这样的叫做权限修饰符,为可选项,创建类时,可以省略权限修饰符,此时默认的修饰符为public。常见的修饰符包括publicprivateprotected。三者的区别如下:

  1. 一般情况下,属性和方法的默认项是public,这意味着属性和方法的各个项从类内部和外部都可以访问。
  2. 用关键字private声明的属性和方法则只能从类的内部访问,也就是只有类内部的方法才可以访问用此关键字声明的类的属性和方法。
  3. 用关键字protected声明的属性和方法也是只能从类内部访问,但是通过“继承”而产生的“子类”也可以访问这些属性和方法。

当一个方法在类定义的内部被调用时,有一个可用的伪变量$this$this是一个到主叫对象的引用(通常是该方法所从属的对象,但如果是从第二个对象静态调用时也可能是另一个对象)。

成员属性

成员属性是指在类中声明的变量。在类中可以声明多个变量,所以对象中可以存在多个成员属性,每个变量将存储不同的对象属性信息。如前例中的$var

成员属性必须使用关键词进行修饰,常见的关键词包括publicprotectedprivate。为了向后兼容,仍然可以使用var关键词修饰,如果直接使用var声明属性,会将其视为public。属性中的变量可以初始化,但是初始化的值必须是常数,这里的常数是指 PHP 脚本在编译阶段时就可以得到其值,而不依赖于运行时的信息才能求值。

成员方法

成员方法是指在类中声明的函数。在类中可以声明多个函数,所以对象中可以存在多个成员方法。类的成员方法可以通过关键字进行修饰,从而控制成员方法的使用权限。

类的实例化(创建类的对象)

在声明一个类之后,并不能直接使用,要使用类的属性和方法,一般需要先实例化一个类,这个实例便是类的对象。

面向对象编程的思想是一切皆为对象。类是对一个事物抽象出来的结果。对象是某类事物中具体的那个。

类和对象可以描述为如下关系:类用来描述具有相同数据结构和特征的“一组对象”,“类”是“对象”的抽象(集合),而“对象”是“类”的具体实例,即一个类中的对象具有相同的“型”,但其中每个对象却具有各不相同的“值”。

创建一个类的实例,必须使用new关键字。类的实例化格式如下:

$变量名=new 类名称([参数]);	// 类的实例化

如果类属于一个命名空间,则必须使用其完整名称。

【示例】

<?php
class SimpleClass {
	// 声明属性
	public $var = 'a default value';
	
	// 声明方法
	public function displayVar() {
		echo $this->var;
	}
}

$instance = new SimpleClass();
// 也可以这样做:
$className = 'SimpleClass';
$instance = new $className();
?>

以上示例创建了类SimpleClass的一个实例instance。如果是在类内部创建实例,可以使用new self来创建新对象。一个类可以实例化多个对象,每个对象都是独立的个体,这些实例化的对象拥有类中定义的全部属性和方法。当对其中一个对象进行操作时,不会影响其他对象。

访问类中成员

通过对象的引用可以访问类中的成员属性和方法,可以用->(对象运算符):$this->property(其中property是属性名),或$this->method(),这种方式访问非静态属性和方法。静态属性和方法则是用::(双冒号):self::$propertyself::method()来访问。

【示例】

<?php
class SimpleClass {
	// 声明属性
	public $var = 'a default value';
	// 声明静态属性
	static $st_var = 'a static var';
	
	// 声明方法
	public function displayVar() {
		echo $this->var;
	}
	//声明静态方法
	static function staticVar(){
		echo self::$st_var;
	}
}

$instance = new SimpleClass();
// 访问非静态属性和方法:
$my_var = $instance->var;
$instance->displayVar();
echo "<br />";

// 访问静态属性和方法
// 静态属性和方法是属于类的,不能通过实例访问
// 只能通过类访问
$my_var1 = SimpleClass::$st_var;
SimpleClass::staticVar();
?>

程序输出结果:

在这里插入图片描述

特殊的访问方法:

  1. $this
    $this存在与类的每一个成员方法中,是一个特殊的对象引用方法。成员方法属于哪个对象,$this引用就代表哪个对象,主要作用是专门完成对象内部成员之间的访问。

  2. 操作符::
    操作符::可以在没有任何实例的情况下访问类中***特定***的成员。语法格式如下:

    关键字::变量名|常量名|方法名
    

    其中,关键字主要包括parentself和类名3中。parent表示可以调用父类中的成员变量、常量和成员方法。self关键字表示可以调用当前类中的常量和静态成员。类名关键字表示可以调用本类中的常量变量和方法。

类常量

可以把类中始终保持不变的值定义为常量。在定义和使用常量的时候不需要使用$符号。常量的值必须是一个定制,不能是变量、类属性、数学运算的结果或函数调用。

类常量的定义使用关键字const

【示例】

<?php
class MyClass {
	const constant = 'constant value';
	
	function showCoustant(){
		echo self::constant."<br />";
	}
}

echo Myclass::constant . "<br />";

$classname = "MyClass";
echo $classname::constant . "<br />";

$class = new MyClass();
$class->showCoustant();

echo $class::constant . "<br />";
?>

程序输出结果:

在这里插入图片描述

静态属性和静态方法

在PHP中,通过static关键词修饰的成员属性和方法称为静态属性和静态方法。静态属性和静态方法可在不被实例化的情况下直接使用。

  1. 静态属性
    在类中,有一个静态属性的概念。和常规属性不一样的是,静态属性属于类本身,而不属于任何实例。因此其也可以称为类属性,以便和对象的属性区分开来。静态属性使用static关键词定义,在类外部可使用“类名::静态属性名”的方式访问,在类内部可使用“self::静态属性名”的方式访问。

    静态属性不能通过一个类已实例化的对象来访问(但静态方法可以)。也就是说静态属性不可以由对象通过->操作符来访问。

    静态属性只能被初始化为字符串或常量,不能使用表达式。所以可以把静态属性初始化为整数或数组,但不能初始化为另一个变量或函数返回值,也不能指向一个对象。

  2. 静态方法
    和静态属性相似,使用static修饰的方法称为静态方法,也可以在不被实例化的情况下使用,其属于类而不是被限制到任何一个特定的对象实例。因此$this在静态方法中不可使用,但可以在对象实例中通过$this->静态方法名的形式调用静态方法,在类内部需要使用self::静态方法名的形式访问。

    用静态方式调用一个非静态方法会导致一个E_STRICT级别错误。

【示例】

<?php
class MyClass {
	// 静态属性
	static $staticVal = 0;
	// 成员属性
	public $val = 100;
	// 静态方法
	static function getStaticVal() {
		// 类内部通过self::静态属性名的方式访问
		echo self::$staticVal . "<br />";
	}
	static function changeStaticVal() {
		self::$staticVal++;
		echo self::$staticVal . "<br />";
		// $this -> val++;
	}
	// 成员方法
	function change(){
		// 在类内部使用$this调用静态方法
		$this->changeStaticVal();
	}
}

// 在类外部通过类名调用静态方法
MyClass::getStaticVal();
MyClass::changeStaticVal();

$obj = new MyClass();
// 通过实例对象调用
$obj->change();
$obj::getStaticVal();	
?>

程序输出结果:

在这里插入图片描述

构造方法和析构方法

PHP允许在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。

构造方法存在与每个声明的类中,如果类中没有直接声明构造方法,那么类会默认生成一个没有参数且内存为空的构造方法。

构造函数使用__construct定义,格式如下:

function __construct([mixed args]){
    // 方法的内容
}

一个类只能声明一个构造方法。构造方法中的参数是可选的,如果没有传入参数,就将使用默认参数为成员变量进行初始化。

如果子类中定义了构造函数则不会隐式调用其父类的构造函数。要执行父类的构造函数,需要在子类的构造函数中调用parent::__construct()。如果子类没有定义构造函数则会如同一个普通的类方法一样从父类继承(假如没有被定义为private的话)。

构造函数是不能返回(return)值的。

【示例】

<?php
class MyClass {
	public $name;
	public $age;
	// 定义构造函数
	function __construct($name, $age){
		$this->name = $name;
		$this->age = $age;
	}
	function get($key){
		return $this->$key;
	}
}

// 不传入参数将报错
// Uncaught ArgumentCountError: Too few arguments to function MyClass::__construct()
// $test1 = new MyClass();
// 传入参数,将使用传入值初始化对象属性
$test2 = new MyClass('Tom', 20);
echo '对象$test2中name属性值为:' . $test2->get('name') . '<br />';
?>

有构造方法,就有它的反面“析构函数”(destructor)。它是在对象被销毁的时候调用执行的。但是因为PHP在每个请求的最终都会把所有资源释放,所以析构函数的意义是有限的。析构函数语法格式如下:

function __destruct(){
    // 方法内容,通常完成一些在对象销毁前的清理任务
}

【示例】

<?php
class MyDestructableClass {
	function __construct(){
		print "进入构造函数<br />";
		$this->name = "MyDestructableClass";
	}
	
	function __destruct(){
		print "销毁" . $this->name . "<br />";
	}
}

$obj = new MyDestructableClass();
?>

程序输出结果:

在这里插入图片描述

和构造函数一样,父类的析构函数不会被隐式调用。要执行父类的析构函数,必须在子类的析构函数体中显示调用parent::__destruct()。此外也和构造函数一样,子类如果自己没有定义析构函数则会继承父类。

析构函数及时在使用exit()终止脚本运行时也会被调用。在析构函数中调用exit()将会终止其余关闭操作的运行。

试图在析构函数(在脚本终止时被调用)中抛出一个异常会导致致命错误。

封装和继承

面向对象的封装特性就是将类中的成员属性和方法内容细节尽可能地隐藏起来,确保类外部代码不能随意访问类中的内容。而面向对象的继承特性使得子类可以继承父类中的属性和方法,提高类代码复用性。

封装

可以使用publicprotectedprivate来修饰对象的属性和方法。使用不同修饰符的属性和方法其可被访问的权限也不同。使用public修饰的属性和方法在任何地方都可以调用,如果在类中的属性和方法前面没有修饰符,默认修饰符为public。使用protected修饰的属性和方法可以在本类和子类中被调用,在其他地方调用将会报错。使用private修饰的属性和方法只能在本类中被访问。

  1. 属性的访问控制
    类属必须定义为公有,受保护,私有之一。如果用var定义,则被视为公有。
    【示例】

    <?php
    /**
     * 定义MyClass类
     */
    class MyClass {
    	// 公有属性
    	public $public = 'Public';
    	// 受保护属性
    	protected $protected = 'Protected';
    	// 私有属性
    	private $private = 'Private';
    	
    	function printHello() {
    		echo $this->public . "<br />";
    		echo $this->protected . "<br />";
    		echo $this->private . "<br />";
    	}
    }
    
    $obj = new MyClass();
    echo $obj->public;		// 这行能被正常执行
    // echo $obj->protected;	// 这行会产生一个致命错误
    // echo $obj->private;		// 这行会产生一个致命错误
    $obj->printHello();		// 这行能被正常执行
    
    /**
     * 定义MyClass2类
     */
    class MyClass2 extends MyClass {
    	// 可以对public和protected进行重定义,但private不行。
    	protected $protected = 'Protected2';
    	
    	function printHello() {
    		echo $this->public . "<br />";
    		echo $this->protected . "<br />";
    		echo $this->private . "<br />";
    	}
    }
    
    $obj2 = new MyClass2();
    echo $obj2->public . "<br />";	// 这行能正常执行
    echo $obj2->private;			// 未定义private
    // echo $obj2->protected;			// 这行会产生一个致命错误
    $obj2->printHello();			// 输出Public Protected2和Undefined
    ?>
    

    程序输出结果:

    在这里插入图片描述

  2. 方法的访问控制
    类中的方法可以被定义为公有,私有或受保护。如果没有设置这些关键字,则该方法默认为公有。
    【示例】

    <?php
    /**
     * 定义MyClass类
     */
    class MyClass {
    	// 声明一个公有的构造函数
    	public function __construct(){ }
    	// 声明一个公有的方法
    	public function MyPublic() { }
    	// 声明一个受保护的方法
    	protected function MyProtected() { }
    	// 声明一个私有的方法
    	private function MyPrivate() { }
    	// 不带访问控制修饰符,默认为公有
    	function Foo() {
    		$this->MyPublic();
    		$this->MyProtected();
    		$this->MyPrivate();
    	}
    }
    
    $myclass = new MyClass;
    $myclass->MyPublic();	// 这样能被正常执行
    $myclass->MyProtected();	// 这行产生一个致命错误
    $myclass->MyPrivate();		// 这样会产生一个致命错误
    $myclass->Foo();			// 公有,受保护,私有都可以执行
    
    /**
     * 定义MyClass2类
     */
    class MyClass2 extends MyClass {
    	// 此方法为公有
    	function Foo2(){
    		$this->MyPublic();
    		$this->MyProtected();
    		$this->MyPrivate();		// 这行会产生一个致命错误
    	}
    }
    
    $myclass2 = new MyClass2;
    $myclass2->MyPublic();		// 这行能正常执行
    $myclass2->Foo2();	// 公有的和受保护的都可以执行,但私有的不行
    
    class Bar {
    	public function test() {
    		$this->testPrivate();
    		$this->testPublic();
    	}
    	
    	public function testPublic() {
    		echo "Bar::testPublic\n";
    	}
    	
    	private function testPrivate() {
    		echo "Bar::testPrivate\n";
    	}
    }
    
    class Foo extends Bar {
    	public function testPublic() {
    		echo "Foo::testPublic\n";
    	}
    	
    	private function testPrivate() {
    		echo "Foo::testPrivate\n";
    	}
    }
    
    $myFoo = new Foo();
    $myFoo->test();		// Bar::testPrivate  foo:testPublic
    ?>
    
  3. 其他对象的访问控制
    同一个类的对象及时不是同一个实例也可以互相访问对方的私有与受保护成员。这是由于在这些对象的内部具体实现的细节都是已知的。
    【示例】

    <?php
    class Test {
    	private $foo;
    	
    	public function __construct($foo) {
    		$this->foo = $foo;
    	}
    	
    	private function bar() {
    		echo '成功访问Test类中的私有方法.<br />';
    	}
    	
    	public function baz(Test $other) {
    		// 我们能改变$other实例的私有属性
    		$other->foo = 'Hello';
    		var_dump($other->foo);
    		
    		// 我们也能调用$other实例的私有方法:
    		$other->bar();
    	}
    }
    
    // 创建类的实例
    $test = new Test('test');
    // 调用baz方法,并传入一个新的实例
    $test->baz(new Test('other'));
    	
    ?>
    

    程序输出结果:

    在这里插入图片描述

继承

继承(inheritance)是OOP中最重要的特性与概念。父类拥有子类的公有属性和方法。子类除了拥有父类的公共属性和方法外,还拥有自己独有的属性和方法。

PHP使用关键字extends来确认子类和父类。继承将会影响到类与类,对象与对象之间的关系。在子类中可以使用parent访问父类的方法。在子类中也可以重写父类中的方法。

【示例】

<?php
class MyClass {
	public $name;
	private $age;
	protected $weight;
	function __construct($name, $age, $weight) {
		$this->name = $name;
		$this->age = $age;
		$this->weight = $weight;
	}
	
	function like() {
		echo "I like PHP。";
	}
	function age(){
		echo $this->name. ' is ' . $this->age . ' years old。';
	}
	protected function get($key) {
		return $this->$key;
	}
	function set($key, $value) {
		$this->$key = $value;
	}
}

class MyClass2 extends MyClass {
	function get($key) {	// 重新父类方法
		echo $this->$key;
	}
	
	function what() {
		parent::like();		// 子类中访问父类方法
	}
	
	function getAge(){
		$this->age();		// 调用从父类继承来的方法
	}
}

// 使用继承自父类的构造方法初始化实例
$obj = new MyClass2('张三', 22, '60kg');
$obj->get('name');
$obj->what();
$obj->set('age', 33);
$obj->getAge();
?>

程序输出结果:

在这里插入图片描述

除非使用了自动加载,否则一个类必须在使用之前被定义。如果一个类扩展了另一个,则父类必须在子类之前被声明。此规则适用于类继承其它类与接口。

final关键字

从PHP5开始,新增了一个final关键字。如果父类中的方法被声明为final,则子类无法覆盖该方法。如果一个类被声明为final,则不能被继承。

【final方法示例】

<?php
class BaseClass {
	public function test() {
		echo "BaseClass::test()方法被调用。<br />";
	}
	final public function moreTesting() {
		echo "BaseClass::moreTesting()方法被调用。<br />";
	}
}

class ChildClass extends BaseClass {
	public function moreTesting() {
		echo "ChildClass::moreTesting()方法被调用。<br />";
	}
}
// 程序将出现致命错误:Fatal error: Cannot override final method BaseClass::moreTesting() 
?>

程序输出结果:

在这里插入图片描述

【final类示例】

<?php
final class BaseClass{
	public function test(){
		echo "BaseClass::test()方法被调用。<br />";
	}
	
	// 这里无论你是否将方法声明为final,都没有关系
	final public function moreTesting() {
		echo "BaseClass::moreTesting()方法被调用。<br />";
	}
}

class ChildClass extends BaseClass {
}
//程序将产生致命错误:Fatal error: Class ChildClass may not inherit from final class (BaseClass)
?>

程序输出结果:

在这里插入图片描述

属性不能被定义为final,只有类和方法才能被定义为final。

抽象类和接口

抽象类

PHP5开始支持抽象类和抽象方法。定义为抽象的类不能被实例化。任何一个类,如果它里面至少有一个方法是被声明为抽象的,那么这个类就必须被声明为抽象的。被定义为抽象的方法只是声明了其调用方式(参数),不能定义具体的功能实现。

继承一个抽象类的时候,子类必须定义父类中的所有抽象方法;另外,这些方法的访问控制修饰必须和父类中一样(或者更为宽松)。如某个抽象方法被声明为受保护的,那么子类中实现的方法就应该声明为受保护的或者公有的,而不能定义为私有的。此外方法的调用方式必须匹配,即类型和参数数量必须一致。

抽象类中的抽象方法不能被定义为私有的(private),同样抽象类中的抽象方法也不能用final修饰。

抽象类使用关键在abstract来声明,抽象方法也必须使用关键abstract来修饰,抽象方法后必须有分号。具体的使用语法格式如下:

abstract class 抽象类名称 {
    // 抽象类的成员变量列表
    abstract function 成员方法1(参数);
    abstract function 成员方法2(参数);
}

【示例】

<?php
// 只要类中有一个抽象方法,类就必须声明为抽象的
abstract class AbstractClass {
	// 强制要求子类定义这些方法
	abstract protected function getValue();
	abstract protected function prefixValue($prefix);
	
	// 普通方法(非抽象方法)
	public function printOut() {
		print $this->getValue() . "<br />";
	}
}

// 子类继承抽象类必须实现抽象类中的抽象方法
class ConcreteClass1 extends AbstractClass {
	protected function getValue() {
		return "ConcreteClass1";
	}
	
	// 子类实现抽象类中的抽象方法时访问权限不能小于抽象方法
	public function prefixValue($prefix) {
		return "{$prefix}ConcreteClass1";
	}
}

class ConcreteClass2 extends AbstractClass {
	public function getValue() {
		return "ConcreteClass2";
	}
	
	public function prefixValue($prefix) {
		return "{$prefix}ConcreteClass2";
	}
}

$class1 = new ConcreteClass1;
$class1->printOut();
echo $class1->prefixValue('Foo_') . "<br />";

$class2 = new ConcreteClass2;
$class2->printOut();
echo $class2->prefixValue('Foo_'). "<br />";
?>

程序输出结果:

在这里插入图片描述

【示例2】

<?php
abstract class AbstractClass {
	// 抽象方法仅需要定义需要的参数
	abstract protected function prefixName($name);
}

// 子类继承抽象类必须实现抽象类中的抽象方法
class ConcreteClass extends AbstractClass {
	// 子类可以定义父类签名中不存在的可选参数
	public function prefixName($name, $separator = ".") {
		if ($name == "Pacman") {
			$prefix = "Mr";
		} elseif ($name == "Pacwoman") {
			$prefix = "Mrs";
		} else {
			$prefix = "";
		}
		return "{$prefix}{$separator}{$name}";
	}
}

$class = new ConcreteClass;
echo $class->prefixName("Pacman"), "<br />";
echo $class->prefixName("Pacwoman"), "<br />";
?>

程序输出结果:

在这里插入图片描述

接口(interface)

PHP只支持单继承,但却可以实现多个接口。声明一个接口的关键字是interface,就像声明一个标准类一样,但其中定义所有的方法都是空的。使用接口可以指定某个类必须实现哪些方法,但不需要定义这些放的具体内容。

接口中定义的所有方法必须是公有的。接口中不能声明变量,只能使用关键字const声明常量的成员属性,但该常量不能被子类或子接口所覆盖。

在接口中定义一个构造方法是被允许的,在有些场景下这可能会很有用,例如用于工厂模式时。

【示例】

<?php
interface Database {
	public function connect($host, $username, $pwd, $db);
	function query($sql);
	function fetch();
	function close();
	function test();
}
?>

接口的实现(implements)

要实现一个接口,使用implements操作符。类中必须实现接口中定义的所有方法,否则会报一个致命错误。类可以实现多个接口,用逗号来分隔多个接口的名称。

实现多个接口时,接口中的方法不能重名。

接口也可以继承,通过使用extends操作符。

类要实现接口,必须使用和接口中所定义的方法完全一致的方式。否则会导致致命错误。

【示例】

<?php
// 声明一个'iTemplate'接口
interface iTemplate {
	public function setVariable($name, $var);
	public function getHtml($template);
}

// 实现接口
// 下面写法是正确的
class Template implements iTemplate {
	private $vars = array();
	
	public function setVariable($name, $var) {
		$this->vars[$name] = $var;
	}
	
	public function getHtml($template) {
		foreach($this->vars as $name=>$value) {
			$template = str_replace('{' . $name . '}', $value, $template);
		}
		return $template;
	}
}

/*
// 下面的写法是错误的,会报错,因为没有实现getHtml():
// Fatal error: Class BadTemplate contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (iTemplate::getHtml) 
class BadTemplate implements iTemplate {
	private $vars = array();
	
	public function setVariable($name, $var) {
		$this->vars[$name] = $var;
	}
}
*/
?>

【示例2—可扩展接口】

<?php
interface a {
	public function foo();
}

interface b extends a {
	public function baz(Baz $baz);
}

// 正确写法
class c implements b {
	public function foo() {
	}
	
	public function baz(Baz $baz) {
	}
}

// 错误写法会导致一个致命错误
// 实现接口,必须使用和接口中所定义的方法完全一致的方式。
// Fatal error: Declaration of d::baz(Foo $foo) must be compatible with b::baz(Baz $baz) 
class d implements b {
	public function foo() {
	}
	
	public function baz(Foo $foo){
	}
}
?>

【示例—继承多个接口】

<?php
interface a {
	public function foo();
}

interface b {
	public function bar();
}

interface c extends a, b {
	public function baz();
}

class d implements c {
	public function foo() {
	}
	public function bar() {
	}
	public function baz() {
	}
}
?>

【示例—实现多个接口】

<?php
interface a {
	public function foo();
}

interface b {
	public function bar();
}

interface c {
	public function baz();
}

class d implements a,b,c {
	public function foo() {
	}
	public function bar() {
	}
	public function baz() {
	}
}
?>

【示例—接口常量】

<?php
interface a {
	const b = 'Interface constant';
}

// 输出接口常量
echo a::b;

// 错误写法,因为常量不能被覆盖,接口常量的概念和类常量一样
// Fatal error: Cannot inherit previously-inherited or override constant b from interface
class b implements a {
	const b = 'class constant';
}
?>

给接口加上类型约束,提供了一种很好的方式来确保某个对象包含有某些方法。

类型约束是PHP5新增的特性,是指函数的参数可以指定必须为对象(在函数原型里面指定类的名字),接口,数组(PHP5.1新增)或者callable(PHP5.4新增)。不过如果使用NULL作为参数的默认值,那么在调用函数的时候依然可以使用NULL作为实参。类型约束不能用于标量类型如intstring

函数调用的参数与定义的参数类型不一致时,会抛出一个可捕获的致命错误。

类型约束不只是用在类的成员函数里,也能使用在普通函数里:

【示例—类型约束】

<?php
class MyClass {
	/**
	 * 测试函数
	 * 第一个参数必须为OtherClass类的一个对象
	 */
	public function test(OtherClass $otherclass) {
		 echo $otherclass->var;
	 }
	 
	/**
	 * 另一个测试函数
	 * 第一个参数必须为数组
	 */
	public function test_array(array $input_array) {
		print_r($input_array);
	}
	
	/**
	 * 第一个参数必须为递归类型
	 */
	public function test_interface(Traversable $iterator) {
		echo get_class($iterator);
	}
	
	/**
	 * 第一个参数必须为回调类型
	 */
	public function test_callable(callable $callback, $data) {
		call_user_func($callback, $data);
	}
}

// OtherClass类定义
class OtherClass {
	public $var = 'Hello World';
}

// 两个类的对象
$myclass = new MyClass;
$otherclass = new OtherClass;

// 致命错误:第一个参数必须是OtherClass类的一个对象
// Fatal error: Uncaught TypeError: Argument 1 passed to MyClass::test() must be an instance of OtherClass, string given, called
$myclass->test('hello');

// 致命错误:第一个参数必须为OtherClass类的一个实例
$foo = new stdClass;
//$myclass->test($foo);

// 致命错误:第一个参数不能为null
$myclass->test(null);

// 正确:输出 Hello World
$myclass->test($otherclass);

// 致命错误:第一个参数必须为数组
$myclass->test_array('a string');

// 正确,输出数组
$myclass->test_array(array('a', 'b', 'c'));

// 正确:输出ArrayObject
$myclass->test_interface(new ArrayObject(array()));

// 正确:输出 int(1)
$myclass->test_callable('var_dump', 1);
?>

Trait

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

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

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

【示例】

<?php
class 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();
?>

程序输出结果:

在这里插入图片描述

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

【示例2】

<?php
trait HelloWorld {
	public function sayHello() {
		echo 'Hello World';
	}
}

class TheWorldIsNotEnough {
	use HelloWorld;
	public function sayHello() {
		echo 'Hello Universe!';
	}
}

$o = new TheWorldIsNotEnough();
$o->sayHello();
?>

程序输出结果:

Hello Universe!

多个trait

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

【示例】

<?php
trait 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();
?>

程序输出结果:

HelloWorld!

解决冲突

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

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

insteadof仅允许排除掉其它方法,as操作符可以为某个方法引入别名。

【示例】

<?php
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;
		A::bigTalk insteadof B;
	}
}

class Aliased_Talker {
	use A, B {
		B::smallTalk insteadof A;
		A::bigTalk insteadof B;
		B::bigTalk as talk;
	}
}

$talk = new Talker;
$talk->smallTalk();
echo "<br />";
$talk->bigTalk();
echo "<br />";

$aliased_talk = new Aliased_Talker;
$aliased_talk->smallTalk();
echo "<br />";
$aliased_talk->bigTalk();
echo "<br />";
$aliased_talk->talk();
?>

程序输出结果:

b

A

b

A

B

在PHP7.0之前,在类里定义和trait同名的属性,哪怕是完全兼容的也会抛出E_STRICT(完全兼容指:具有相同的访问可见性、初始默认值)。

修改方法的访问控制

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

【示例】

<?php
trait HelloWorld {
	public function sayHello() {
		echo 'HelloWorld!';
	}
}

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

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

在trait中使用trait

在trait定义时通过使用一个或多个trait,能够组合其他trait中的部分或全部成员。

【示例】

<?php
trait 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!

trait中的抽象成员

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

【示例】

<?php
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;
	}
}

$o = new MyHelloWorld;
$o->setWorld(' World!');
$o->sayHelloWorld();
?>

程序输出结果:

Hello World!

trait的静态成员

trait可以定义静态成员和静态方法:

【示例】

<?php
trait Counter {
	public function inc() {
		static $c = 0;
		$c = $c + 1;
		echo "$c<br />";
	}
    public static function doSomething() {
        return 'Doing something<br />';
    }
}
class C1 {
	use Counter;
}

class C2 {
	use Counter;
}
$o = new C1;
$o->inc();		// echo 1
echo C1::doSomething();
$p = new C2;
$p->inc();		// echo 1
echo C2::doSomething();
?>

程序输出结果:

1
Doing something
1
Doing something

trait属性

trait同样可以定义属性。当trait定义了一个属性后,类就不能定义同样名称的属性,否则会产生致命错误。有种情况列外:属性是兼容的(同样的访问可见度、初始默认值)。在PHP7.0之前,属性是兼容的,则会有E_STRICT的提醒。

【示例】

<?php
trait PropertiesTrait {
	public $same = true;
	public $different = false;
}

class PreopertiesExample {
	use PropertiesTrait;
	public $same = true;	// PHP7.0之后没有问题,之前版本是E_STRICT提醒。
	public $different = true;	// 致命错误
}
?>

匿名类

PHP7开始支持匿名类。匿名类很有用,可以创建一次性的简单对象。

【示例】

<?php
// PHP7之前
class Logger {
	public function log($msg) {
		echo $msg;
	}
}
$util->setLogger(new Logger));

// PHP7+后
$util->setLogger(new class {
	public function log($msg) {
		echo $msg;
	}
});
?>

可以传递参数到匿名类的构造器,也可以扩展(extends)其他类、实现接口(implements interface),以及像其它普通的类一样使用trait:

【示例】

<?php
class SomeClass{}
interface SomeInterface {}
trait SomeTrait{}

var_dump(new class(10) extends SomeClass implements SomeInterface {
	private $num;
	public function __construct($num) {
		$this->num = $num;
	}
	use SomeTrait;
});
?>

程序输出结果:

object(class@anonymous)#1 (1) { ["num":"class@anonymous":private]=> int(10) }

匿名类被嵌套进普通Class后,不能访问这个外部类(Outerclass)的private(私有)、protected(受保护)方法或者属性。为了访问外部类(Outerclass)protected属性或方法,匿名类可以extend(扩展)此外部类。为了使用外部类(Outerclass)的private属性,必须通过构造器传递:

【示例】

<?php
class Outer {
	private $prop = 1;
	protected $prop2 = 2;
	protected function func1() {
		return 3;
	}
	public function func2() {
		return new class($this->prop) extends Outer {
			private $prop3;
			public function __construct($prop) {
				$this->prop3 = $prop;
			}
			public function func3() {
				return $this->prop2 + $this->prop3 + $this->func1();
			}
		};
	}
}
echo (new Outer)->func2()->func3();
?>

匿名类的名称是通过引擎赋予的。由于实现的细节,不应该去依赖这个类名。

声明的同一个匿名类,所创建的对象都是这个类的实例。

【示例】

<?php
function anonymous_class() {
	return new class {};
}

if(get_class(anonymous_class()) === get_class(anonymous_class())) {
	echo 'same class';
} else {
	echo 'diferent class';
}
?>

程序输出结果:

same class

多态

多态性是指同一操作作用于不同类的实例,将产生不同的执行结果,即不同类的对象收到相同的消息时,得到不同的结果。在PHP中,实现多态的方法有两种,包括通过继承实现多态和通过接口实现多态。

通过继承实现多态

instanceof关键字可以检测对象属于哪个类,也可以用于检测生成实例的类是否继承自某个接口。可以通过instanceof来实现多态。

【示例】

<?php
// 定义抽象类Vegetables
abstract class Vegetables {
	// 定义抽象方法,通过继承让不同的类实现相同的方法
	abstract function go_Vegetables();
}
// 定义马铃薯类继承蔬菜类
class Vegetables_potato extends Vegetables {
	// 实现go_Vegetables方法
	public function go_vegetables(){
		echo "我们开始种植马铃薯。<br />";
	}
}
// 定义萝卜类也继承蔬菜类
class Vegetables_radish extends Vegetables {
	// 实现go_Vegetables方法
	public function go_Vegetables(){
		echo "我们开始种植萝卜。<br />";
	}
}
// 自定义一个方法来根据对象调用不同的方法实现多态
function change($obj) {
	if ($obj instanceof Vegetables){
		$obj->go_Vegetables();
	}else{
		echo "传入的参数不是一个有效对象";
	}
}

echo "实例化Vegetables_potato:";
change(new Vegetables_potato());
echo "实例化Vegetables_radish:";
change(new Vegetables_radish());
?>

程序输出结果:

在这里插入图片描述

通过接口实现多态

接口实现多态的方式与继承的类似,只是使用的是接口而不是抽象类。

【示例】

<?php
// 定义接口Vegetables
interface Vegetables {
	// 定义接口方法
	public function go_Vegetables();
}

// 定义Vegetables_potato实现Vegetables接口
class Vegetables_potato implements Vegetables {
	// 定义go_Vegetables方法
	public function go_Vegetables(){
		echo "我们开始种植马铃薯<br />";
	}
}

// 定义Vegetables_radish实现Vegetables接口
class Vegetables_radish implements Vegetables {
	// 定义go_Vegetables方法
	public function go_Vegetables() {
		echo "我们开始种植萝卜<br />";
	}
}

// 自定义方法根据对象调用不同的方法
function change($obj) {
	if ($obj instanceof Vegetables) {
		$obj->go_Vegetables();
	} else {
		echo "传入的参数不是一个有效的对象";
	}
}
echo "实例化Vegetables_potato:";
change(new Vegetables_potato());
echo "实例化Vegetables_radish:";
change(new Vegetables_radish());
?>

程序输出结果:

在这里插入图片描述

魔术方法

PHP提供了内置的拦截器,也称为魔术方法,它可以“拦截”发送到未定义方法和属性的消息。魔术方法通常以两个下划线__开头。

常见的魔术方法有:

__construct() // 构造方法,在实例化一个类时,触发
__destruct()  // 析构方法,在一个实例对象被销毁的时候触发
__call()	  // 访问一个不能访问的成员方法时触发
__get()	//获得一个不能访问的成员属性时触发
__set()	//设置一个不能访问的成员属性时触发
__isset() //在一个不可访问的对象属性被isset,empty时触发
__unset()	//在一个不可访问的对象属性被unset时触发
__sleep()//在对一个对象的数据处理,不需要保存全部数据时触发,如: serialize()序列化时
__wakeup()//在unserialize()时,会先检查是否有__wakeup方法
__toString()//将一个对象实例被当成字符串时触发
__invoke()//当以函数的方式调用对象时触发
__set_state()//当一个对象被var_export时触发
__clone()//在创建一个新对象时触发
__debuginfo()//当一个对象被var_dump时触发
__callStatic($name, $arguments)//访问一个不能访问的成员静态方法时触发

在命名自己的类方法时不能使用以上方法名,除非是想使用其魔术功能。

PHP将所有以__(两个下划线)开头的类的方法保留为魔术方法。所以在定义类方法时,除了以上魔术方法,建议不要以__为前缀。

__sleep()__wakeup()

这两个魔术方法主要用户对象的序列化和反序列化,这里先暂时做个简单的了解,在后续对象序列化小节再详细说明。

对象序列化函数serialize()会检查类中是否存在一个魔术方法__sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则NULL被序列化,并产生一个E_NOTICE级别的错误。

public __sleep(void):array

与之相反,反序列化函数unserialize()会检查是否存在一个__wakeup()方法。如果存在,则会先调用,预先准备对象需要的资源。

__wakeup(void):void

__toSring()

该魔术方法用于一个类被当成字符串时应怎样回应。如echo $obj;应该显示什么。此方法必须返回一个字符串,否则将抛出E_RECOVERABLE_ERROR级别的致命错误。

【示例】

<?php
// 定义一个简单的类,未包含__toString()方法
class TestClass {
	public $foo;
	public function __construct($foo) {
		$this->foo = $foo;
	}
}
// 再定义一个简单的类,包含__toString()方法
class TestClass1 {
	public $foo;
	public function __construct($foo) {
		$this->foo = $foo;
	}
	public function __toString(){
		return $this->foo;
	}
}

$class1 = new TestClass1('Hello');
echo $class1;		// __toString()方法被自动调用
$class = new TestClass('Hello');
// TestClass未定义__toString()方法,将报错
//Recoverable fatal error: Object of class TestClass could not be converted to string
echo $class;
?>

程序输出结果:

在这里插入图片描述

不能在__toString()方法中抛出异常,这么做会导致致命错误。

__invoke()

当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用。

__invoke([$...]):mixed

本特性在PHP5.3.0及以上版本有效

【示例】

<?php
class CallableClass {
	function __invoke($x) {
		var_dump($x);
	}
}

$obj = new CallableClass;
$obj(5);
var_dump(is_callable($obj));
?>

程序输出结果:

int(5) bool(true)

从以上代码可以看到,当定义了__invoke()方法后,类就可以被当做一个函数来调用了。

__set_state()

改方法为一个静态方法,当调用var_export()导出类时,此静态方法会被调用。

static __set_state(array $properties):object

本方法的唯一参数是一个数组,其中包含array('property'=>value, ...)格式排列的类属性。

【示例】

<?php
class A {
	public $var1;
	public $var2;
	public static function __set_state($an_array) {
		$obj = new A;
		$obj->var1 = $an_array['var1'];
		$obj->var2 = $an_array['var2'];
		return $obj;
	}
}
$a = new A;
$a->var1 = 5;
$a->var2 = 'foo';
eval('$b = ' . var_export($a, true) . ';');
var_dump($b)
?>

程序输出结果:

object(A)#2 (2) { [“var1”]=> int(5) [“var2”]=> string(3) “foo” }

__debuginfo()

此方法在转储对象以获取应显示的属性时由var_dump()调用。如果未在对象上定义该方法,则将会显示所有公共、受保护和私有属性。

【示例】

<?php
class A {
	private $prop;
	public $prop1;
	public function __construct($val) {
		$this->prop = $val;
	}
	public function __debugInfo() {
		return [
			'propSquared' => $this->prop ** 2,
		];
	}
}

class B {
	private $prop;
	public $prop1;
	public function __construct($val) {
		$this->prop = $val;
	}
}

echo "<pre>";
var_dump(new A(42));
echo "<br>";
var_dump(new B(42));
?>

程序输出结果:

object(A)#1 (1) {
  ["propSquared"]=>
  int(1764)
}
object(B)#1 (2) {
  ["prop":"B":private]=>
  int(42)
  ["prop1"]=>
  NULL
}

实现重载的魔术方法

PHP所提供的“重载”(overloading)是指动态地“创建”类属性和方法。是通过魔术方法来实现的。

当调用当前环境下未定义或不可见的类属性或方法时,重载方法会被调用。所有的重载方法都必须声明为public。

PHP中的“重载”与其他绝大多数面向对象语言不同。传统的“重载”是用于提供多个同名的类方法,但各方法的参数类型和个数不同。

所有针对重载的魔术方法的参数都不能通过引用传递。

  1. 属性重载
    public __set(string $name, mixed $value):void
    public __get(string $name):mixed
    public __isset(string $name):bool
    public __unset(string $name):void
    在给不可访问属性赋值时,__set()会被调用。
    读取不可访问属性值时,__get()会被调用。
    当对不可访问属性调用isset()empty()时,__isset()会被调用。
    当对不可访问属性调用unset()时,__unset()会被调用。
    其中$name是指要操作的变量名称,__set()方法的$value参数指定了$name变量的值。
    属性重载只能在对象中进行。在静态方法中,这些魔术方法将不会被调用。所以这些方法都不能声明为static

    【示例】

    <?php
    class PropertyTest {
    	/** 被重载的数据保存在此 */
    	private $data = array();
    	
    	/** 重载不能被用在已经定义的属性 */
    	public $declared = 1;
    	
    	/** 只有从类外部访问这个属性时,重载才会发生 */
    	private $hidden = 2;
    	
    	public function __set($name, $value) {
    		echo "Setting '$name' to '$value'<br />";
    		$this->data[$name] = $value;
    	}
    	public function __get($name) {
    		echo "Getting '$name'<br />";
    		if (array_key_exists($name, $this->data)) {
    			return $this->data[$name];
    		}
    		$trace = debug_backtrace();
    		trigger_error(
    			'Undefined property via __get(): '. $name .
    			' in ' . $trace[0]['file'] .
    			' on line ' . $trace[0]['line'],
    			E_USER_NOTICE);
    		return null;
    	}
    	
    	/** php5.1之后版本 */
    	public function __isset($name) {
    		echo "Is '$name' set?<br />";
    		return isset($this->data[$name]);
    	}
    	/** php5.1之后版本 */
    	public function __unset($name) {
    		echo "Unsetting '$name'<br />";
    		unset($this->data[$name]);
    	}
    	
    	/** 非魔术方法 */
    	public function getHidden() {
    		return $this->hidden;
    	}
    }
    
    echo "<pre><br />";
    $obj = new PropertyTest;
    
    $obj->a = 1;
    echo $obj->a . "<br />";
    
    var_dump(isset($obj->a));
    unset($obj->a);
    var_dump(isset($obj->a));
    echo "<br />";
    
    echo $obj->declared . "<br />";
    echo "我们来测试一下私有属性'hidden':<br />";
    echo "私有属性存在于类中, 所以__get()未被调用...<br />";
    echo $obj->getHidden() . "<br />";
    echo "私有属性在类外部不可见, 因此__get()方法被调用 ...<br />";
    echo $obj->hidden . "<br />";	
    ?>
    

    程序输出结果:
    在这里插入图片描述

    因为PHP处理赋值运算的方式,__set()的返回值将被忽略。类似的,在下面这样的链式赋值中,__get()不会被调用:

    $a = $obj->b = 8;

    在除isset()外的其他语言结构中无法使用重载的属性,这意味着当对一个重载的属性使用empty()时,重载魔术方法将不会被调用。

  2. 方法重载
    public __call (string $name, array $arguments):mixed
    public static __callStatic (string $name, array $arguments):mixed
    在对象中调用一个不可访问的方法时,__call()会被调用。
    在静态上下文中调用一个不可访问方法时,__callStatic()会被调用。
    其中$name参数是要调用的方法名称。$arguments参数是一个枚举数组,包含着要传递给方法$name的参数。
    【示例】

    <?php
    class MethodTest {
    	public function __call($name, $arguments) {
    		// 注意:$name的值区分大小写
    		echo "Calling object method '$name' "
    		     . implode(', ', $arguments) . "<br/>";
    	}
    	
    	/** php5.3之后版本 */
    	public static function __callStatic($name, $arguments) {
    		// 注意:$name的值区分大小写
    		echo "Calling static method '$name' "
    		     . implode(', ', $arguments) . "<br />";
    	}
    }
    
    $obj = new MethodTest;
    $obj->runTest('in object context');
    
    MethodTest::runTest('in static context');
    ?>
    

    程序输出结果:

    Calling object method ‘runTest’ in object context
    Calling static method ‘runTest’ in static context

对象复制

对象复制可以通过clone关键字来完成(如果可能,这将调用对象的__clone()方法)。对象中的__clone()方法不能被直接调用。

当使用clone复制对象时,PHP会对对象的所有属性执行一个浅复制(shallow copy)。所有的引用属性仍然会是一个指向原来的变量的引用。

当复制完成时,如果定义了__clone()方法,则新创建的对象(复制生成的新对象)中的__clone()方法会被调用,可用于修改属性的值(如果有必要的话)。

【示例】

<?php
class SubObject {
	static $instances = 0;
	public $instance;
	
	public function __construct() {
		$this->instance = ++self::$instances;
	}
	public function __clone() {
		$this->instance = ++self::$instances;
	}
}

class MyCloneable {
	public $object1;
	public $object2;
	
	function __clone() {
		// 强制赋值一份$this->object,否则仍然指向同一个对象
		$this->object1 = clone $this->object1;
	}
}

$obj = new MyCloneable();

$obj->object1 = new SubObject();
$obj->object2 = new SubObject();

$obj2 = clone $obj;

echo "<pre>";
print("原对象:<br />");
print_r($obj);

print("克隆对象:<br />");
print_r($obj2);
?>

程序输出结果:

原对象:MyCloneable Object
(
    [object1] => SubObject Object
        (
            [instance] => 1
        )

    [object2] => SubObject Object
        (
            [instance] => 2
        )

)
克隆对象:MyCloneable Object
(
    [object1] => SubObject Object
        (
            [instance] => 3
        )

    [object2] => SubObject Object
        (
            [instance] => 2
        )

)

以上代码中,当执行$obj2 = clone $obj;时,首先MyCloneable__clone()先被调用,然后执行__clone()中的代码$this->object1 = clone $this->object1;,此时SubObject中的__clone()被调用,于是instance的值变成了3。

对象的操作

对象比较

当使用比较运算符(==)比较两个对象变量时,比较的原则是:如果两个对象的属性和属性值都相等,而且两个对象是同一个类的实例,那么这两个对象变量相等。

而如果使用全等运算符(===),这两个对象变量一定要指向某个类的同一个实例(即同一个对象)。

【示例】

<?php
function bool2str($bool) {
	if ($bool === false) {
		return 'FALSE';
	} else {
		return 'TRUE';
	}
}

function compareObjects(&$o1, &$o2) {
	echo 'o1 == o2 : ' . bool2str($o1 == $o2) . "<br />"; 
	echo 'o1 != o2 : ' . bool2str($o1 != $o2) . "<br />"; 
	echo 'o1 === o2 : ' . bool2str($o1 === $o2) . "<br />"; 
	echo 'o1 !== o2 : ' . bool2str($o1 !== $o2) . "<br />"; 
}

class Flag {
	public $flag;
	function __construct($flag = true) {
		$this->flag = $flag;
	}
}

class OtherFlag {
	public $flag;
	function __construct($flag = true) {
		$this->flag = $flag;
	}
}

$o = new Flag();
$p = new Flag();
$q = $o;
$r = new OtherFlag();

echo "同一个类的两个实例进行比较的结果:<br />";
compareObjects($o, $p);

echo "同一个实例的两个引用进行比较的结果:<br />";
compareObjects($o, $q);

echo "<br />两个类的两个实例进行比较的结果:<br />";
compareObjects($o, $r);
?>

程序输出结果:

在这里插入图片描述

对象遍历

PHP提供了一种定义对象的方法使其可以通过单元列表来遍历,如用foreach语句。默认情况下,所有可见属性都将别用于遍历。

【示例】

<?php
class MyClass {
	public $var1 = 'value 1';
	public $var2 = 'value 2';
	public $var3 = 'value 3';
	
	protected $protected = 'protected var';
	private $private = 'private var';
	
	function iterateVisible() {
		echo "MyClass::iterateVisible:<br />";
		foreach($this as $key => $value) {
			print "$key => $value<br />";
		}
	}
}

$class = new MyClass;
foreach($class as $key=>$value) {
	print "$key => $value<br />";
}
echo "<br />";

$class->iterateVisible();
?>

程序输出结果:

在这里插入图片描述

从输出结果可以看出,在类的外部,protected和private修饰的变量都是不可见的,所以直接遍历对象,不会遍历这两种修饰符修饰的属性。而函数iterateVisible()在类的内部,所以所有属性都可见。

对象的遍历只会遍历属性,不会变量方法。

还可以通过实现Iterator接口的方式,让对象自行决定如何遍历以及每次遍历时哪些值可用。

【示例】

<?php
class MyIterator implements Iterator {
	private $var = [];
	
	public function __construct($array) {
		if (is_array($array)) {
			$this->var = $array;
		}
	}
	
	// 实现Iterator中的方法rewind()
	public function rewind() {
		echo "重置指针<br />";
		reset($this->var);
	}
	// 实现Iterator中的方法current()
	public function current() {
		$var = current($this->var);
		echo "当前元素值:$var<br />";
		return $var;
	}
	// 实现Iterator中的方法key() 
	public function key() {
		$var = key($this->var);
		echo "键值:$var<br />";
		return $var;
	}
	// 实现Iterator中的方法next()
	public function next() {
		$var = next($this->var);
		echo "下一个元素值:$var<br />";
		return $var;
	}
	// 实现Iterator中的方法valid()
	public function valid() {
		$var = $this->current() !== false;
		echo "指针是否有效:{$var}<br />";
		return $var;
	}
}

$values = [1, 2, 3];
$it = new MyIterator($values);

foreach($it as $a=>$b) {
	print "$a: $b<br />";
}
?>

程序输出结果:

在这里插入图片描述

还可以用IteratorAggregate接口替代实现所有的Iterator方法。IteratorAggregate只需实现一个方法IteratorAggregate::getIterator(),其应返回一个实现了Iterator的类的实例。

【示例】

<?php
class MyIterator implements Iterator {
	private $var = [];
	
	public function __construct($array) {
		if (is_array($array)) {
			$this->var = $array;
		}
	}
	
	// 实现Iterator中的方法rewind()
	public function rewind() {
		echo "重置指针<br />";
		reset($this->var);
	}
	// 实现Iterator中的方法current()
	public function current() {
		$var = current($this->var);
		echo "当前元素值:$var<br />";
		return $var;
	}
	// 实现Iterator中的方法key() 
	public function key() {
		$var = key($this->var);
		echo "键值:$var<br />";
		return $var;
	}
	// 实现Iterator中的方法next()
	public function next() {
		$var = next($this->var);
		echo "下一个元素值:$var<br />";
		return $var;
	}
	// 实现Iterator中的方法valid()
	public function valid() {
		$var = $this->current() !== false;
		echo "指针是否有效:{$var}<br />";
		return $var;
	}
}
class MyCollection implements IteratorAggregate {
	private $items = [];
	private $count = 0;
	
	// 必须实现这个方法
	public function getIterator() {
		return new MyIterator($this->items);
	}
	
	public function add($value) {
		$this->items[$this->count++] = $value;
	}
}

$coll = new MyCollection;
$coll->add('value 1');
$coll->add('value 2');
$coll->add('value 3');

foreach($coll as $key=>$val) {
	echo "key/value:[$key -> $val]<br />";
}
?>

程序输出结果:

在这里插入图片描述

对象和引用

我们通常说,在默认情况下对象是通过引用传递的。但其实这并不完全正确。

PHP的引用是别名,就是说两个不同的变量的名字指向相同的内容。在PHP中一个对象变量保存的并非是整个对象的值,而是保存了一个标识符来访问真正的对象内容。当对象作为参数传递,作为结果返回,或者赋值给另一个变量时,另一个变量跟原来的变量不是引用关系,只是他们都保存着同一个标识符的拷贝,这个标识符指向同一个对象的真正内容。

在这里插入图片描述

【示例】

<?php
class A {
	public $foo = 1;
}

$a = new A;
$b = $a;		// $a,$b都是同一个标识符的拷贝
				// ($a) = ($b) = <id>
$b->foo = 2;
echo $a->foo."<br />";

$c = new A;
$d = &$c;		// $c, $d是引用
				// ($c, $d) = <id>
$d->foo = 2;
echo $c->foo."<br />";

$e = new A;
function foo($obj) {
	// ($obj) = ($e) = <id>
	$obj->foo = 2;
}
foo($e);
echo $e->foo."<br />";
?>

程序输出结果:

2
2
2

对象序列化

所有php中的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串便会php原来的值。序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。

为了能够unserialize()一个对象,这个对象的类必须已经定义过。如序列化A类的对象,将会返回一个跟A类相关,而且包含了对象所有变量值的字符串。如果要想在另外一个文件中反序列化一个对象,这个对象的类必须在反序列化之前定义,可以通过包含一个定义该类的文件或者使用函数spl_autoload_register()来实现类的自动加载。

【示例】

// classa.inc
<?php
class A {
	public $one = 1;
	public function show_one() {
		echo $this->one;
	}
}
?>
// page1.php
<?php
include("classa.inc");

$a = new A;
$s = serialize($a);
// 把变量$s保存起来以便文件page2.php能够读取
file_put_contents('store', $s);
?>
    
// page2.php
<?php
// page2.php
// 要正确了解序列化,必须包含下面一个文件
include("classa.inc");

$s = file_get_contents('store');
$a = unserialize($s);

// 现在可以使用对象$a里面的函数show_one()
$a->show_one();
?>

类的自动加载

在编写面向对象(OOP)程序时,很多开发者为每个类新建一个php文件。如果要开发一个大型的应用,那么可能会出现每个脚本开头,都需要包含(include)一个长长的列表(每个类都有个文件)。

spl_autoload_register()函数可以注册任意数量的自动加载器,当使用尚未被定义的类(class)和接口(interface)时自动去加载。通过注册自动加载器,脚本引擎在PHP出错失败前有了最后一个加会加载所需的类。

自动加载不可用于PHP的CLI交互模式。

【示例】

尝试从MyClass1.php和Myclass2.php文件中加载MyClass1和Myclass2类。

<?php
spl_autoload_register(function ($class_name) {
    require_once $class_name . '.php';
});

$obj = new MyClass1();
$obj2 = new MyClass2();
?>

当程序在找不到类定义时,会自动执行spl_autoload_register()函数,将类名作为参数传入,尝试加载以类名为文件名的脚本文件,如果加载失败会抛出一个异常。

  • 7
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值