PHP预定义接口知识梳理

作者: 乘风御上者 分类: PHP 发布时间: 2020-07-22 10:49

开发PHP多年对预定义接口方面多多少少接触过,至今没有个系统的认识。趁着有时间将这块知识梳理下,方便记忆。
(所有梳理内容建立在PHP7版本之上,更低版本差异自动忽略)

官方手册已经将预定义接口分类明确:

  1. Traversable 遍历接口
  2. Iterator 迭代器接口
  3. IteratorAggregate 聚合式迭代器接口
  4. ArrayAccess 数组式访问接口
  5. Serializable 序列化接口
  6. Closure 闭包类
  7. Generator 生成器类

Traversable:遍历接口

该接口非常特殊,不能被继承或实现。唯一的用处就是通过使用 instanceof 判断一个类是否实现了该接口功能。所有实现该接口功能的类都将能被遍历操作(foreach)。而且该类被 Iterator 与 IteratorAggregate 所继承。

Iterator:迭代器接口

// 先解读内部方法
Iterator extends Traversable {

	// 返回当前游标指向的元素
	abstract public current ( void ) : mixed
	// 返回当前游标指向的元素对应的键名
	abstract public key ( void ) : scalar
	// 将游标指向下一个元素
	abstract public next ( void ) : void
	// 重置游标,将其指向第一个元素
	abstract public rewind ( void ) : void
	// 判断当前游标指向的是否是元素
	abstract public valid ( void ) : bool
}

因为该接口继承自 Traversable 所有实现该接口的类可以被 foreach 遍历。当使用 foreach 遍历对应的类时,以上类方法将在不同时机被调用:

  1. 执行遍历前,rewind方法将被先执行,说明每次遍历都将先重置游标。
  2. 遍历开始,valid方法将被执行,判断当前游标元素是否存在,存在继续,不存在则停止遍历。
  3. 读取当前元素与键名,current与key函数被执行。
  4. 最后将游标指向(next执行)下一个元素,进入下一轮遍历(从步骤2开始)。
class MyIterator implements Iterator {
    private $key = 0;
    private $data = [];

    public function __construct(Array $data) {
        echo '<pre>初始化<br>';
        $this->data = $data;
    }

    public function rewind() {
        var_dump('重置游标');
        $this->key = 0;
    }

    public function current() {
        var_dump('得到当前元素的值');
        return $this->data[$this->key];
    }

    public function key() {
        var_dump('得到当前元素的键名');
        return $this->key;
    }

    public function next() {
        var_dump('将游标指向下一个元素');
        ++$this->key;
    }

    public function valid() {
        var_dump('元素是否有效');
        return isset($this->data[$this->key]);
    }
}

$data = [
    "Element001",
    "Element002",
    "Element003",
];
$mi = new MyIterator($data);
// foreach相当于上面四步的封装
foreach($mi as $k => $v) {
    echo '当前值:'.$v;
    echo "<br>-----------------------------<br>";
}

IteratorAggregate:聚合式迭代器接口

该接口与Iterator接口一样继承自Traversable接口。它主要是用来将Iterator接口要求实现的5个迭代器方法委托给其他类的。它可以让你在类的外部实现迭代功能,并允许重新使用常用的迭代器方法,而不是在编写的每个可迭代类中重复这些方法。

// 内部继承
IteratorAggregate extends Traversable {

	// 返回一个继承自Traversable的实例类(因其无法继承,所有一般是实现Iterator的)
	abstract public getIterator ( void ) : Traversable
}

实例化IteratorAggregate与实例化Iterator一样,都是生成一个迭代器类。不同的是它不需要像Iterator一样再去实现5个方法,只需要实现getIterator方法,并返回一个已经实现Iterator接口的类的实例即可。

要实现这个接口,必须先实现Iterator接口,是不是感觉有点多余?别急看代码:

// 实现聚合迭代器
class MyIteratorAggregate implements IteratorAggregate {
    // 数据
    protected $data = ['one','two','three'];

    // 必须实现的方法
    public function getIterator(){
        // 返回一个实现Iterator接口类的实例
        // 偷懒用上面的那个类
        return new MyIterator($this->data);
    }
}

其实IteratorAggregate接口就是为简化类似Iterator接口而来。当你需要多个迭代器类,都去实现Iterator接口时,就要每次都实现5个方法,是不是代码很冗余?

有了IteratorAggregate接口,只需要实现一次Iterator接口,以后所有的迭代器实现都交给IteratorAggregate接口就好,如此代码简洁多了。

其实PHP的SPL扩展早就为我们做好了相关简化。它有个ArrayIterator类,就是继承自Iterator接口。

它仿佛就是专门为了给实现IteratorAggregate接口的getIterator方法而存在的。也就是说我们实现getIterator方法其实不需要直接写一个实现Iterator接口的类,直接使用SPL内置的ArrayIterator就可以了。

// 如此完美搞定
public function getIterator(){
    // 将上例中 MyIterator 替换成 ArrayIterator 即可
    return new ArrayIterator($this->arr);
}

ArrayAccess:数组式访问接口

实现对象像数组一样的访问方式去访问对象的属性。

// 在框架中大量使用,简单粗暴好用!
ArrayAccess {
	// 检测偏移位置的值是否存在
	abstract public offsetExists ( mixed $offset ) : boolean
	// 获取偏移位置的值
	abstract public offsetGet ( mixed $offset ) : mixed
	// 设置偏移位置的值
	abstract public offsetSet ( mixed $offset , mixed $value ) : void
	// 复位(删除)偏移位置的值
	abstract public offsetUnset ( mixed $offset ) : void
}

// 实例化一个对象
class MyObject implements ArrayAccess {
    private $container = [];

    public function offsetSet($offset, $value) {
    	echo '设置偏移位置的值<br>';
        if (is_null($offset)) {
            $this->container[] = $value;
        } else {
            $this->container[$offset] = $value;
        }
    }

    public function offsetExists($offset) {
    	echo '检测偏移位置的值是否存在<br>';
        return isset($this->container[$offset]);
    }

    public function offsetUnset($offset) {
    	echo '删除偏移位置的值<br>';
        unset($this->container[$offset]);
    }

    public function offsetGet($offset) {
    	echo '获取偏移位置的值<br>';
        return isset($this->container[$offset]) ? $this->container[$offset] : null;
    }
}

$cls = new MyObject();

// 自动调用offsetSet方法
$cls['name'] = 'TOOCF';

// 自动调用offsetGet方法
$cls['name'];

// 自动调用offsetExists方法
isset($cls["age"]);

// 自动调用offsetUnset方法
unset($cls["name"]);

Serializable:序列化接口

该接口官方说的比较详细:实现此接口的类将不再支持 __sleep() 和 __wakeup()。不论何时,只要有实例需要被序列化,serialize 方法都将被调用。它将不会调用 __destruct() 或有其他影响,除非程序化地调用此方法。当数据被反序列化时,类将被感知并且调用合适的 unserialize() 方法而不是调用 __construct()。

Serializable {
	// 相当于魔术方法 __sleep
	abstract public serialize ( void ) : string
	// 相当于魔术方法 __wakeup
	abstract public unserialize ( string $serialized ) : mixed
}

目前查询很多资料并未找到与sleep与wakeup的大区别,只在序列化后的字符上稍微有一点区别,貌似该区别也没啥用。

Closure:闭包

// 所有的闭包函数返回的都是一个闭包实例
Closure {
	// 用于禁止实例化的构造函数
	__construct ( void )
	// 将闭包对象绑定到指定的$this对象和类作用域上并返回新闭包类
	public static bind ( Closure $closure , object $newthis [, mixed $newscope = 'static' ] ) : Closure
	// 同上。不同点是一个静态调用,一个方法调用方式。
	public bindTo ( object $newthis [, mixed $newscope = 'static' ] ) : Closure
}

bind与bindTo作用一样,不同的是调用方式。
bind为静态调用 Closure::bind(所要操作的闭包, 闭包指向的类实例, 闭包的作用域)
bindTo的调用方式为 所要操作的闭包->bindTo() 省去bind的第一个参数

此处比较重要的一点就是作用域$newscope,控制着这个匿名函数的访问范围。
默认的访问范围是全局,当传入一个类时,则将访问范围控制在此类的访问范围上。

这里要结合类属性的可访问权限去理解:
public: 共有类型。可在子类中访问和实例化后随意调用
protected: 受保护类型。可在子类中访问,但不能再实例化后调用
private: 私有类型。只能在自己类中使用,子类和不可访问,实例化后也不能调用

回到上面作用域$newscope,默认访问范围是全局时,相当于在实例化后访问,所有只能访问类的public类型。
当指定一个类时,则相当于在类内部访问,则三种权限类型都可访问。

// 定义一个类
class Test {
	// 定义一个私有属性
    private $name = 'toocf';
    private static $age = 18;
}

// 将闭包函数赋予一个变量(变量赋值结束的分号一定不要忘记!!)
$getName = function () {
    return $this->name;
};

// 该变量就是一个闭包类实例
var_dump($getName);

// 此时执行闭包函数将报错
// 因为根据上下文找不到$this指向
$getName();

// 给闭包中的$this增加指向,返回新闭包
$getName = $getName->bindTo(new Test);

// 此时$this指向Test实例
// 但是因为在绑定时,没有指定作用域,所有只能访问该类public作用域下的属性
// 因name是私有属性,所有报错
var_dump($getName());

// 增加作用域为该类内部
// 参二可以是Test的实例或者字符串'Test'(注意要写全命名空间,所有写Test::class自动写完整)
$getName = $getName->bindTo(new Test, Test::class);

// 得到 "toocf"
var_dump($getName());

// 访问静态变量
$getAge = function () {
	return self::$age;
};

// 访问静态变量唯一不同就是指向的实例化类可以省略
$getAge1 = $getAge->bindTo(null, Test::class);
// 完毕
var_dump($getAge1());


Generator:生成器

呃,本篇内容有点过长。这样吧,咱们下回再说!

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复