类与对象 - PHP手册笔记

原文:类与对象 - PHP手册笔记

基本概念

PHP对待对象的方式与引用和句柄相同,即每个变量都持有对象的引用,而不是整个对象的拷贝。

当创建新对象时,该对象总是被赋值,除非该对象定义了构造函数并且在出错时抛出了一个异常。类应在被实例化之前定义。

创建对象时,如果该类属于一个名字空间,则必须使用其完整名称。

在类定义内部,可以用new selfnew parent创建对象。

<?php 
$instance = new stdClass();
$assigned = $instance;
$reference = & $instance;
$instance->var = '$assigned will have this value.';
$instance = null;
var_dump($instance);
var_dump($reference);
var_dump($assigned);

这段代码的输出如下,这是为什么呢?

null
null
object(stdClass)[1]
  public 'var' => string '$assigned will have this value.' (length=31)

PHP 5.3引进了两个新方法来创建一个对象的实例,可以使用下面的方法创建实例。

<?php 
class Test {
	static public function getNew() {
		return new static;
	}
}
class Child extends Test {}
$obj1 = new Test();
$obj2 = new $obj1;
var_dump($obj1 !== $obj2);  // true
$obj3 = Test::getNew();
var_dump($obj3 instanceof Test);  // true
$obj4 = Child::getNew();
var_dump($obj4 instanceof Child);  // true
var_dump($obj1 == $obj2);  // true

PHP不支持多重继承,被继承的方法和属性可以通过同样的名字重新声明被覆盖,注意参数必须保持一致,当然构造函数除外。但是如果父类定义方法时使用了final,则该方法不可被覆盖。可以通过parent::来访问被覆盖的方法和属性,parent::只能访问父类中的常量const,不能访问变量。

<?php 
class A {
	private $name = 'A';
	const conname = 'A';
	public function getName() {
		return $this->name;
	}
}
class B extends A {
	private $name = 'B';
	const conname = 'B';
	public function getName() {
		return $this->name;
	}
	public function getParent() {
		return parent::conname;
	}
}
class C extends B {
	private $name = 'C';
	const conname = 'C';
	public function getName() {
		return $this->name;
	}
	public function getParent() {
		return parent::conname;
	}
}
$a = new A;
var_dump($a->getName());  // A
$b = new B;
var_dump($b->getName());  // B
var_dump($b->getParent());  // A
$c = new C;
var_dump($c->getName());  // C
var_dump($c->getParent());  // B

自PHP 5.5起,关键词class也可用于类名的解析。使用ClassName::class你可以获取一个字符串,包含了类ClassName的完全限定名称。

<?php 
namespace NS {
	class ClassName {}
	echo ClassName::class;  // NS\ClassName
}

属性

属性,也就是类的变量成员。属性中的变量可以初始化,但是初始化的值必须是常数。这里的常数是指 PHP 脚本在编译阶段时就可以得到其值,而不依赖于运行时的信息才能求值。

在类的成员方法里,访问非静态属性使用$this->property,访问静态属性使用self::$property。静态属性声明时使用static关键字。

类常量

在定义常量时不需要$符号和访问控制关键字。

接口(interface)中也可以定义常量。

自动加载类

写面向对象的应用程序时,通常对每个类的定义简历一个PHP源文件。当某个文件需要调用这些类时,需要在文件开头写一个长长的包含文件列表。其实,并不需要这样,可以定义一个__autoload()函数,它会在试图使用尚未被定义的类时自动调用。

手册Tip说,spl_autoload_register()提供了一种更加灵活的方式来实现类的自动加载,这个后面再看。

自动加载不可用于PHP的CLI交互模式,也就是命令行模式。

用户输入中可能存在危险字符,起码要在__autoload()时验证下输入。

可以通过下面的方式自动加载类。

<?php 
function __autoload($class_name) {
	require_once $class_name.'.php';
}
$obj1 = new MyClass1();
$obj2 = new MyClass2();

对于异常处理,后面再看。

构造函数和析构函数

PHP 5允许开发者在一个类中定义一个方法作为构造函数,构造函数也不支持重载。

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

与其它方法不同,当__construct()与父类__construct()具有不同参数时,可以覆盖。

自PHP 5.3.3起,在命名空间中,与类名同名的方法不再作为构造函数。

析构函数会在某个对象的所有引用都被删除或者对象被显示销毁时执行。析构函数即使在使用exit()终止脚本运行时也会被调用。

试图在析构函数中抛出异常,将会导致致命错误。

访问控制

类属性必须定义为公有、受保护、私有之一,不能省略关键字。如果类中方法没有设置访问控制的关键字,则该方法默认为公有。

同一个类的对象,即使不是同一个实例,也可以互相访问对方的私有与保护成员。示例程序如下。

<?php 
Class Test {
	private $foo;
	public function __construct($foo) {
		$this->foo = $foo;
	}
	private function bar() {
		echo 'Accessed the private method.';
	}
	public function baz(Test $other) {
		$other->foo = 'hello';
		var_dump($other->foo);
		$other->bar();
	}
}
$test = new Test('test');
$test->baz(new Test('other'));

对象继承

如果一个类扩展了另一个,则父类必须在子类前被声明。

范围解析操作符

范围解析操作符,简单地说就是一对冒号,可以用于访问静态成员、类常量,还可以用于调用父类中的属性和方法。

当在类定义之外引用这些项目时,要使用类名。

static

使用static关键字可以用来定义静态方法和属性,也可用于定义静态变量以及后期静态绑定。声明类属性或方法为静态,就可以不实例化类而直接访问。

静态属性不能通过一个类已实例化的对象来访问,但静态方法可以。

如果没有指定访问控制,属性和方法默认为公有。

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

抽象类

PHP 5支持抽象类和抽象方法。类中如果有一个抽象方法,那这个类必须被声明为抽象的。

抽象类不能被实例化。抽象方法只是声明了其调用方式(参数),不能定义其具体的功能实现。继承抽象类时,子类必须定义父类中的所有抽象方法,且这些方法的访问控制必须和父类一样活更宽松。

方法的调用方式必须匹配。但是,子类定义了一个可选参数,而父类抽象方法的声明里没有,则两者的声明并无冲突。这也试用与PHP 5.4起的构造函数。可以在子类中定义父类签名中不存在的可选参数。

<?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');
echo $class->prefixName('Pacwoman');

对象接口

听说过接口,一直没用过。使用接口,可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容,也就是说接口中定义的所有方法都是空的。接口中定义的所有方法都必须是公有的,这是接口的特性。

接口也可以继承多个接口,用逗号分隔,使用extends操作符。类中必须实现接口中定义的所有方法,否则会报错。要实现一个接口,使用implements操作符。类可以实现多个接口,用逗号分隔。实现多个接口时,接口中的方法不能有重名。类要实现接口,必须使用和接口中所定义的方法完全一致的方式。

接口中也可定义常量。接口常量和类常量的使用完全相同,但是不能被子类或子接口覆盖。

traits

从PHP 5.4.0开始,可以使用traits实现代码复用。Traits 是一种为类似 PHP 的单继承语言而准备的代码复用机制。Trait 不能通过它自身来实例化。它为传统继承增加了水平特性的组合。

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

通过逗号分隔,在 use 声明列出多个 trait,可以都插入到一个类中。如果两个 trait 都插入了一个同名的方法,如果没有明确解决冲突将会产生一个致命错误,为解决冲突,需使用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;
		B::bigTalk as talk;
	}
}
$t = new Talker();
$t->smallTalk();  // b
$t->bigTalk();  // A
$t->talk();  // B

使用as操作符还可以用来调整方法的访问控制,或者给方法一个改变了访问控制的别名,原版方法的访问控制规则没有改变。

<?php 
trait HelloWorld {
	public function sayHello() {
		echo 'Hello World.';
	}
}
class MyClass1 {
	use HelloWorld {
		sayHello as protected;
	}
}
class MyClass2 {
	use HelloWorld {
		sayHello as private myPrivateHello;
	}
}

就像类能够使用trait那样,多个trait能够组合为一个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;
	}
}
$c = new MyHelloWorld;
$c->setWorld('world');
$c->sayHelloWorld();

如果trait定义了一个属性,那类将不能定义同样名称的属性,否则会产生错误。

重载

PHP提供的重载是指动态地创建类属性和方法,与其它绝大多数面向对象语言不同。通过魔术方法来实现。当使用不可访问的属性或方法时,重载方法会被调用。所有的重载方法都必须被声明为public

使用__get()__set()__isset()__unset()进行属性重载,示例如下。

<?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];
		}
		return null;
	}
	public function __isset($name) {
		echo "Is $name set? <br>";
		return isset($this->data[$name]);
	}
	public function __unset($name) {
		echo "Unsetting $name. <br>";
		unset($this->data[$name]);
	}
}
$obj = new PropertyTest;
$obj->a = 1;
var_dump($obj->a);
var_dump(isset($obj->a));
unset($obj->a);
var_dump(isset($obj->a));
var_dump($obj->declared);
var_dump($obj->hidden);

输出结果如下:

Setting a to 1. 
Getting a. 
int 1
Is a set? 
boolean true
Unsetting a. 
Is a set? 
boolean false
int 1
Getting hidden. 
null

在对象中调用一个不可访问方法时,__call()会被调用。用静态方式中调用一个不可访问方法时,__callStatic()会被调用。参数为调用方法的名称和一个枚举数组,注意区分大小写。

使用__call()__callStatic()对方法重载,示例如下。

<?php 
class MethodTest {
	public function __call($name, $arguments) {
		echo "Calling object method $name " . 
			implode(', ', $arguments) . '<br>';
	}
	public static function __callStatic($name, $arguments) {
		echo "Calling static method $name " . 
			implode(', ', $arguments) . '<br>';
	}
}
$obj = new MethodTest;
$obj->runTest('in object context');
MethodTest::runTest('in static context');

遍历对象

对象可以用过单元列表来遍历,例如用foreach语句。默认所有可见属性都将被用于遍历。

<?php 
class MyClass {
	public $var1 = 'value 1';
	public $var2 = 'value 2';
	public $var3 = 'value 3';
	private $var4 = 'value 4';
	protected $var5 = 'value 5';
}
$obj = new MyClass;
foreach($obj as $key => $value) {
	echo "$key => $value <br>";
}

示例程序2实现了Iterator接口的对象遍历,示例程序3通过实现IteratorAggregate来遍历对象。

魔术方法

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

前面遇到过的魔术方法有:__construct()__destruct()__call()__callStatic()__get()__set()__isset()__unset()。后面将会介绍:__sleep()__wakeup()__toString()__invoke()__set_state()__clone()__debugInfo()

__sleep__wakeup不清楚具体做什么用的,示例程序中给出了个数据库连接的例子。

__toString方法用于一个类被当成字符串时应怎样回应。此方法必须返回一个字符串,且不能再方法中抛出异常。如果将一个未定义__toString()方法的对象转换为字符串,将产生错误。

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

当调用var_export()导出类时,__set_state()会被调用。

当调用var_dump()时,__debugInfo会被调用。PHP 5.6新加入,没合适的环境无法测试。

final

果父类中的方法被声明为final,则子类无法覆盖该方法。如果一个类被声明为final,则不能被继承。属性不能被定义为final,只有类和方法才能被定义为final

对象复制

多数情况,我们不需要完全复制一个对象,但有时确实需要。对象复制可以通过clone关键字来完成。这种复制是通过调用对象的__clone()方法实现的,但是对象中的__clone()方法不能被直接调用。

对象比较

比较运算符==为真的条件是:两个对象的属性和属性值都相等,而且两个对象是同一个类的实例。

继承与统一个基类的两个子类的对象不会相等==

<?php 
class Base {}
class A extends Base {}
class B extends Base {}
$a = new A;
$b = new B;
var_dump($a == $b);  // false

全等运算符===为真的条件是:两个对象变量一定要指向某个类的同一个实例(即同一个对象)。

类型约束

类型约束是指函数的参数可以指定必须为对象、接口、数组或者callable类型。但是类型约束不能用于标量类型如intstringtraits也不允许。类型约束允许NULL值。

后期静态绑定

后期静态绑定,用于在继承范围内引用静态调用的类。

转发调用,指的是通过以下几种方式进行的静态调用:self::parent::static::以及forward_static_call()

后期静态绑定的工作原理是,存储了上一个非转发调用的类名。

当进行静态方法调用时,该类名即为明确指定的那个;当进行非静态方法调用时,即为该对象所属的类。

使用self::或者__CLASS__对当前类的静态引用,取决于定义当前方法所在的类。

<?php 
class A {
	public static function who() {
		echo __CLASS__;
	}
	public static function test() {
		self::who();
	}
}
class B extends A {
	public static function who() {
		echo __CLASS__;
	}
}
B::test();  // A
B::who();  // B

static::关键字表示运行时最初调用的类,后期静态绑定就是这样使用。如下面程序所示,也就是说调用test()时引用的类是B而不是A

<?php 
class A {
	public static function who() {
		echo __CLASS__;
	}
	public static function test() {
		static::who();
	}
}
class B extends A {
	public static function who() {
		echo __CLASS__;
	}
}
B::test();  // B
B::who();  // B

示例2给出的是非静态环境下使用static::

后期静态绑定的解析,会一直到取得一个完全解析了的静态调用为止。另外,如果静态调用使用parent::self::将转发调用信息。

<?php 
class A {
	public static function foo() {
		static::who();
	}
	public static function who() {
		echo __CLASS__;
	}
}
class B extends A {
	public static function test() {
		A::foo();
		parent::foo();
		self::foo();
	}
	public static function who() {
		echo __CLASS__;
	}
}
class C extends B {
	public static function who() {
		echo __CLASS__;
	}
}
C::test();  // ACC

那么问题来了,结果为什么是这样的呢?

对象和引用

默认情况下,对象时通过引用传递的。但这种说法不完全正确,其实两个对象变量不是引用的关系,只是他们都保存着同一个标识符的拷贝,这个标识符指向同一个对象的真正内容。

对象序列化

所有PHP里面的值,都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变为原来的值。

序列化一个对象,将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。为了能够unserialize()一个对象,这个对象的类必须已经定义过。在应用程序中序列化对象以便在之后使用,强烈推荐在整个应用程序都包含对象的类的定义。

(全文完)

上一篇:DataWorks百问百答11:数据地图中数据组织和查找有哪几种方式?


下一篇:实战DeviceIoControl 之三:制作磁盘镜像文件