SPL 库也叫做 PHP 标准库,主要就是用于解决典型问题的一组接口或类的集合。这些典型问题包括什么呢?比如我们今天要讲的数据结构,还有一些设计模式的实现,就像我们之前讲过的观察者模式相关的接口在 SPL 库中都有提供。话说回来,在 PHP 中,由于语言的特点,其实很多数据结构都和我们用 C 语言实现的略有不同,比如说链表,由于没有结构的概念,所以我们一般会使用类来代表链表的结点。除了这个之外,要手写链表还需要链表的增、删、改、查等操作,而 SPL 库中其实已经帮我们提供了一个双向链表的实现,并且还可以在这个链表的基础上直接实现栈和队列的操作。

双向链表

在 SPL 库中,双向链表只需要实例化一个 SplDoublyLinkedList 类就可以了,然后我们就可以对这个实例化之后的双向链表对象进行各种操作。

$dll = new SplDoublyLinkedList();

var_dump($dll->isEmpty()); // bool(true)

$dll->push(200);
$dll->push(300);
$dll->unshift("五号");
$dll->add(2, "六号"); var_dump($dll->isEmpty()); // bool(false) var_dump($dll);
// object(SplDoublyLinkedList)#1 (2) {
// ["flags":"SplDoublyLinkedList":private]=>
// int(0)
// ["dllist":"SplDoublyLinkedList":private]=>
// array(4) {
// [0]=>
// string(6) "五号"
// [1]=>
// int(200)
// [2]=>
// string(6) "六号"
// [3]=>
// int(300)
// }
// }

从代码中可以看出,push() 、 unshift() 、add() 方法都是向链表中添加数据,而 isEmpty() 则用于判断链表是否为空。直接打印显示链表的内容,可以看到链表的内部是一个数组数据。

var_dump($dll->top()); // int(300)
var_dump($dll->bottom()); // string(6) "五号" var_dump($dll->pop()); // int(300)
var_dump($dll->shift()); // string(6) "五号" var_dump($dll->serialize()); // string(25) "i:0;:i:200;:s:6:"六号";"
var_dump($dll->count()); // int(2)

top() 和 bottom() 分别获取的是链表的顶部和底部的数据。而 pop() 和 shift() 则是分别从底部和顶部弹出数据。后面我们会看到,根据设置的不同,它他们也会遵循使用栈还是队列的方式来弹出数据。

serialize() 方法可以直接获得序列化后的链表内容。count() 方法就是返回链表内元素的数量了。

$dll->offsetSet(1, '修改成新六号');
var_dump($dll->offsetGet(1)); // string(18) "修改成新六号"
var_dump($dll->offsetExists(1)); // bool(true)
$dll->offsetUnset(1);
var_dump($dll->offsetExists(1)); // bool(false)

offset 相关的方法函数是根据偏移值来操作链表内的数据,其实就可以理解成是根据位置下标来操作数据。

在默认情况下,我们遍历链表的话,是类似于队列的形式进行输出的,也就是先进先出的状态。

for($i=1;$i<5;$i++){
$dll->push($i);
} var_dump($dll->getIteratorMode()); // int(0)
$dll->rewind();
while($dll->valid()){
echo '============', PHP_EOL;
echo 'key:', $dll->key(), PHP_EOL;
echo ' current:', $dll->current(), PHP_EOL;
$dll->next();
}
// ============
// key:0
// current:200
// ============
// key:1
// current:1
// ============
// key:2
// current:2
// ============
// key:3
// current:3
// ============
// key:4
// current:4

通过 rewind() 将链表指针恢复到开头,然后通过 valid() 方法判断当前数据是否有效,next() 用于将链表指针移动到下一个,就可以进行数据的遍历。我们通过设置链表的迭代模式,就可以改变链表的迭代输出规则,比如我们需要类似栈类型的后进先出。

$dll->setIteratorMode(SplDoublyLinkedList::IT_MODE_LIFO);
$dll->rewind();
while($dll->valid()){
echo 'IT_MODE_LIFO============', PHP_EOL;
echo 'key:', $dll->key(), PHP_EOL;
echo ' current:', $dll->current(), PHP_EOL;
$dll->next();
}
// IT_MODE_LIFO============
// key:4
// current:4
// IT_MODE_LIFO============
// key:3
// current:3
// IT_MODE_LIFO============
// key:2
// current:2
// IT_MODE_LIFO============
// key:1
// current:1
// IT_MODE_LIFO============
// key:0
// current:200

另外,它还有一个比较好玩的迭代模式,就是直接边遍历,边删除。

$dll->setIteratorMode(SplDoublyLinkedList::IT_MODE_DELETE);
$dll->rewind();
while($dll->valid()){
echo 'IT_MODE_DELETE============', PHP_EOL;
echo 'key:', $dll->key(), PHP_EOL;
echo ' current:', $dll->current(), PHP_EOL;
$dll->next();
}
var_dump($dll);
// object(SplDoublyLinkedList)#1 (2) {
// ["flags":"SplDoublyLinkedList":private]=>
// int(1)
// ["dllist":"SplDoublyLinkedList":private]=>
// array(0) {
// }
// }

在使用 IT_MODE_DELETE 进行遍历之后,链表里面的数据内容也就变成空的了。默认情况下,这个 IteraotrMode 的内容是 SplDoublyLinkedList::IT_MODE_KEEP | SplDoublyLinkedList::IT_MODE_FIFO 这个值,表示的就是数据保持原来的状态并且使用先进先出的规则。

栈类 SplStack 其实和后面的队列类 SplQueue 一样,都是继承自链表类的,也就是说它们其实就是相当于设置好了 IteratorMode 的链表对象。所以它们的方法函数其实都没有什么太大的区别。

// 栈
$stack = new SplStack();
for($i=1;$i<5;$i++){
$stack->push($i);
}
var_dump($stack->getIteratorMode()); // int(6)
var_dump($stack);
// object(SplStack)#2 (2) {
// ["flags":"SplDoublyLinkedList":private]=>
// int(6)
// ["dllist":"SplDoublyLinkedList":private]=>
// array(4) {
// [0]=>
// int(1)
// [1]=>
// int(2)
// [2]=>
// int(3)
// [3]=>
// int(4)
// }
// } $stack->rewind();
while($stack->valid()){
echo '============', PHP_EOL;
echo 'key:', $stack->key(), PHP_EOL;
echo ' current:', $stack->current(), PHP_EOL;
$stack->next();
}
// ============
// key:3
// current:4
// ============
// key:2
// current:3
// ============
// key:1
// current:2
// ============
// key:0
// current:1

队列

SplQueue 队列相对于链表类和栈类来说,多了两个方法。

// 队列
$queue = new SplQueue();
for($i=1;$i<5;$i++){
$queue->enqueue($i);
}
var_dump($queue->getIteratorMode()); // int(4)
var_dump($queue);
// object(SplQueue)#3 (2) {
// ["flags":"SplDoublyLinkedList":private]=>
// int(4)
// ["dllist":"SplDoublyLinkedList":private]=>
// array(4) {
// [0]=>
// int(1)
// [1]=>
// int(2)
// [2]=>
// int(3)
// [3]=>
// int(4)
// }
// } $queue->rewind();
while($queue->valid()){
echo '============', PHP_EOL;
echo 'key:', $queue->key(), PHP_EOL;
echo ' current:', $queue->current(), PHP_EOL;
echo ' info:', $queue->dequeue(), PHP_EOL;
$queue->next();
}
// ============
// key:0
// current:1
// info:1
// ============
// key:1
// current:2
// info:2
// ============
// key:2
// current:3
// info:3
// ============
// key:3
// current:4
// info:4

enqueue() 和 dequeue() 方法分别就是入队和出队的意思,其实就可以看成是 push() 和 shift() 的操作,底部添加顶部弹出。

堆栈堆栈,总会有人把堆和栈说成是一个东西,其实它们可是完全不同的两个数据结构哦。栈是线性的逻辑结构,而堆则一般是树形的逻辑结构,当然,它们的存储结构都是可以用相同的链表或顺序表来表示的。在堆中,有大顶堆和小顶堆的概念,SPL 也为我们分别提供了这两种实现。(不了解堆的同学可以自行查阅相关资料)

大顶堆

$maxHeap = new SplMaxHeap();
for($i=1;$i<5;$i++){
$maxHeap->insert($i);
} var_dump($maxHeap);
// object(SplMaxHeap)#4 (3) {
// ["flags":"SplHeap":private]=>
// int(0)
// ["isCorrupted":"SplHeap":private]=>
// bool(false)
// ["heap":"SplHeap":private]=>
// array(4) {
// [0]=>
// int(4)
// [1]=>
// int(3)
// [2]=>
// int(2)
// [3]=>
// int(1)
// }
// } var_dump($maxHeap->count()); // int(4)
var_dump($maxHeap->top()); // int(4) var_dump($maxHeap->extract()); // int(4) var_dump($maxHeap->count()); // int(3)
var_dump($maxHeap->top()); // int(3) var_dump($maxHeap);
// object(SplMaxHeap)#4 (3) {
// ["flags":"SplHeap":private]=>
// int(0)
// ["isCorrupted":"SplHeap":private]=>
// bool(false)
// ["heap":"SplHeap":private]=>
// array(3) {
// [0]=>
// int(3)
// [1]=>
// int(1)
// [2]=>
// int(2)
// }
// } $maxHeap->rewind();
while($maxHeap->valid()){
echo '============', PHP_EOL;
echo 'key:', $maxHeap->key(), PHP_EOL;
echo ' current:', $maxHeap->current(), PHP_EOL;
$maxHeap->next();
}
// ============
// key:2
// current:3
// ============
// key:1
// current:2
// ============
// key:0
// current:1 var_dump($maxHeap->isCorrupted()); // bool(false)
$maxHeap->recoverFromCorruption();

SplMaxHeap 类就是用于生成大顶堆实例的类模板。它通过 insert() 方法插入数据,通过 extract() 方法抽取数据,同样也包括 count() 和 top() 这类的常用方法函数,以及遍历相关的那些方法函数。

另外,堆的操作中还包括两个方法函数,分别用于判断堆是否处于损坏状态 isCorrupted() 以及从损坏状态恢复 recoverFromCorruption() 相关的操作函数。

小顶堆

小顶堆的内容和大顶堆就完全一样了,只是它的内部结构不同,大顶堆是父结点总是最大的,而小顶堆就是反过来父结点总是最小的数据。

$minHeap = new SplMinHeap();
for($i=1;$i<5;$i++){
$minHeap->insert($i);
}
var_dump($minHeap);
// object(SplMinHeap)#5 (3) {
// ["flags":"SplHeap":private]=>
// int(0)
// ["isCorrupted":"SplHeap":private]=>
// bool(false)
// ["heap":"SplHeap":private]=>
// array(4) {
// [0]=>
// int(1)
// [1]=>
// int(2)
// [2]=>
// int(3)
// [3]=>
// int(4)
// }
// } var_dump($minHeap->top()); // int(1)

大顶堆实现的优先队列

除了大顶堆和小顶堆的普通操作之外,SPL 库中还有一个通过大顶堆来实现的优先队列的类模板。

$pQueue = new SplPriorityQueue();
for($i=1;$i<5;$i++){
$pQueue->insert($i, random_int(1,10));
}
var_dump($pQueue);
// object(SplPriorityQueue)#6 (3) {
// ["flags":"SplPriorityQueue":private]=>
// int(1)
// ["isCorrupted":"SplPriorityQueue":private]=>
// bool(false)
// ["heap":"SplPriorityQueue":private]=>
// array(4) {
// [0]=>
// array(2) {
// ["data"]=>
// int(3)
// ["priority"]=>
// int(10)
// }
// [1]=>
// array(2) {
// ["data"]=>
// int(4)
// ["priority"]=>
// int(7)
// }
// [2]=>
// array(2) {
// ["data"]=>
// int(1)
// ["priority"]=>
// int(3)
// }
// [3]=>
// array(2) {
// ["data"]=>
// int(2)
// ["priority"]=>
// int(2)
// }
// }
// } while($pQueue->valid()){
var_dump($pQueue->extract());
}
// int(3)
// int(4)
// int(1)
// int(2)

它的操作方法函数和堆的操作方法函数都是一样的,只是 insert() 方法中多了一个参数可以设置数据的优先级。通过设置不同的优先级我们可以看到数据以及遍历输出的结果都会发生变化,顺序都是以优先级来确定的。

固定数组

什么叫固定数组呢?在 PHP 中,数组这个结构非常强大,它即可以是普通下标类型的数组,也可以 HashMap键值对 形式的数组,它的长度也是不受限制的,只要内存够就可以灵活地处理数组的长度。不过在静态语言中,特别是我们学习过的 C 语言中,数组都是固定长度的,也就是说,数组的内存大小是在数组初始化的时候就确定好的,如果超出了数组长度的操作发生,就会产生越界问题。还是通过一个例子来看吧。

// 数组
$norArr = [];
$norArr[1] = 'b';
$norArr[4] = 'f'; var_dump($norArr);
// array(2) {
// [1]=>
// string(1) "b"
// [4]=>
// string(1) "f"
// } $fArr = new SplFixedArray(5);
$fArr[1] = 'b';
$fArr[4] = 'f'; var_dump($fArr);
// object(SplFixedArray)#7 (5) {
// [0]=>
// NULL
// [1]=>
// string(1) "b"
// [2]=>
// NULL
// [3]=>
// NULL
// [4]=>
// string(1) "f"
// }

norArr 是普通的 PHP 数组,我们添加了两个数据之后在这个数组中只有两个元素。下面的 SplFixedArray 类实例化出来的 fArr 则是固定数组。它在实例化的时候必须传递一个构造参数来指定数组长度。可以看到,fArr 输出的结果是固定有 5 个数据的,并且我们没有赋值的数据都会给一个默认的 NULL 值。是不是和 C 的数组一样一样的。

当然,固定数组就会有数组下标越界的问题了。

$fArr[6] = 'h'; // Fatal error: Uncaught RuntimeException: Index invalid or out of range

不过我们可以手动地修改数组的大小来改变数据的长度。

$fArr->setSize(7);
$fArr[6] = 'h';
var_dump($fArr->getSize()); // int(7)

现在,数组的长度就是 7 了,可以存放 7 条数据。它也可以直接从一个普通数组转换过来,不过需要注意的是,转换数组必须是数字下标类型的数组,字符串键的 HashMap 数组是不可以的哦。

$fArr2 = SplFixedArray::fromArray(range(1,3));
var_dump($fArr2);
// object(SplFixedArray)#8 (3) {
// [0]=>
// int(1)
// [1]=>
// int(2)
// [2]=>
// int(3)
// } // $fArr3 = SplFixedArray::fromArray(['new'=>1, 'old'=>2]);
// var_dump($fArr3);
// PHP Fatal error: Uncaught InvalidArgumentException: array must contain only positive integer keys

剩下的就是和其它数据结构一样的一些操作方法函数了。

var_dump($fArr->count()); // int(7)

var_dump($fArr->offsetGet(2)); // NULL
$fArr->offsetSet(2, 'new');
var_dump($fArr->offsetGet(2)); // string(3) "new"
var_dump($fArr->offsetExists(2)); // bool(true)
$fArr->offsetUnset(2);
var_dump($fArr->offsetExists(2)); // bool(false) $fArr->rewind();
while($fArr->valid()){
echo '============', PHP_EOL;
echo 'key:', $fArr->key(), PHP_EOL;
echo ' current:', $fArr->current(), PHP_EOL;
$fArr->next();
}
// ============
// key:0
// current:
// ============
// key:1
// current:b
// ============
// key:2
// current:
// ============
// key:3
// current:
// ============
// key:4
// current:f
// ============
// key:5
// current:
// ============
// key:6
// current:h

既然它是一种数组对象,那么迭代其实不用这么麻烦的,我们直接通过 for 和 foreach 就可以了。它和其它的数组结构一样,都实现了 Iterator 和 Countable 这两个接口,都是可以通过 for 和 foreach 来进行遍历的。

foreach($fArr as $f){
var_dump($f);
}
for($i=0;$i<count($fArr);$i++){
var_dump($fArr[$i]);
}

对象数据映射

最后一种数据结构,对象数据映射。这是个什么鬼?最简单直接的理解其实就是把一个对象当成是 【键】,然后以这些键形成一个数组结构。

$os = new SplObjectStorage();

$o1 = new stdClass;
$o2 = new stdClass;
$o3 = new stdClass;
$o4 = new stdClass;
$o5 = new stdClass; $os->attach($o1);
$os->attach($o2);
$os->attach($o3);
$os->attach($o4, 'd4');
$os->attach($o5, 'd5'); var_dump($os);
// object(SplObjectStorage)#9 (1) {
// ["storage":"SplObjectStorage":private]=>
// array(5) {
// ["00000000736a0aba000000002f97228d"]=>
// array(2) {
// ["obj"]=>
// object(stdClass)#10 (0) {
// }
// ["inf"]=>
// NULL
// }
// ["00000000736a0abb000000002f97228d"]=>
// array(2) {
// ["obj"]=>
// object(stdClass)#11 (0) {
// }
// ["inf"]=>
// NULL
// }
// ["00000000736a0abc000000002f97228d"]=>
// array(2) {
// ["obj"]=>
// object(stdClass)#12 (0) {
// }
// ["inf"]=>
// NULL
// }
// ["00000000736a0abd000000002f97228d"]=>
// array(2) {
// ["obj"]=>
// object(stdClass)#13 (0) {
// }
// ["inf"]=>
// string(2) "d4"
// }
// ["00000000736a0abe000000002f97228d"]=>
// array(2) {
// ["obj"]=>
// object(stdClass)#14 (0) {
// }
// ["inf"]=>
// string(2) "d5"
// }
// }
// }

是不是有点意思,attach() 就可以向这个 SplObjectStorage 对象存储映射类中添加数据。它的第二个参数可以指定一个数据内容,其实就可以看作是普通数组中的 值 。

var_dump($os->count()); // int(5)
$os->detach($o2);
var_dump($os->count()); // int(4) var_dump($os->contains($o2)); // bool(false)
var_dump($os->contains($o3)); // bool(true) var_dump($os->getHash($o4)); // string(32) "000000003e67a2330000000040e598c9" var_dump($os->offsetGet($o4)); // string(2) "d4"
$os->offsetSet($o4, 'new d4');
var_dump($os->offsetGet($o4)); // string(6) "new d4"
var_dump($os->offsetExists($o4)); // bool(true)
$os->offsetUnset($o4);
var_dump($os->offsetExists($o4)); // bool(false) $os->rewind();
$os[$o1] = 'new d1';
while($os->valid()){
echo '============', PHP_EOL;
echo 'key:', $os->key(), PHP_EOL;
if($os->getInfo() === NULL){
$os->setInfo('new iter info');
}
echo ' info:', $os->getInfo(), PHP_EOL;
echo ' current:', PHP_EOL;
var_dump($os->current()); $os->next();
}
// ============
// key:0
// info:new d1
// current:
// object(stdClass)#10 (0) {
// }
// ============
// key:1
// info:new iter info
// current:
// object(stdClass)#12 (0) {
// }
// ============
// key:2
// info:d5
// current:
// object(stdClass)#14 (0) {
// }

其它的遍历查询操作都是和其它数据结构的操作类似的,这里就不多说了。其中比较特别的是 detach() 方法是删除数据的,getHash() 则是获取这个对象在存储集合中的 Hash 值的,这个值也可以看做是这个对象在这个对象映射集合中的下标,我们其它的针对对象的操作判断其实是都是在内部转换成这个数组下标来进行操作的。

总结

其实这一圈学习下来,突然发现有了 SPL 的这几个数据结构之后,我们在 PHP 下面还真不太需要关心什么数据结构方面的实现了,直接通用点就上个双向链表就完了,简单的就只是写算法了。好吧,学习还是要扎实点,数据结构和算法真正要学习的其实是它内部的思想和逻辑。当然,既然已经提供了,那么我们平常的业务开发中还是更建议直接使用 SPL 的这些数据结构来处理!

测试代码:

https://github.com/zhangyue0503/dev-blog/blob/master/php/2021/01/source/3.PHP的SPL扩展库(一)数据结构.php

参考文档:

https://www.php.net/manual/zh/spl.datastructures.php

PHP的SPL扩展库(一)数据结构的更多相关文章

  1. PHP SPL标准库之数据结构栈(SplStack)介绍(基础array已经可以解决很多问题了,现在开始解决问题)

    PHP SPL标准库之数据结构栈(SplStack)介绍(基础array已经可以解决很多问题了,现在开始解决问题) 一.总结 SplStack就是继承双链表(SplDoublyLinkedList)实 ...

  2. PHP SPL扩展库简单使用

    1. __autoload 这是一个自动加载函数,在PHP5中,当我们实例化一个未定义的类时,就会触发此函数.看下面例子: ./myClass.php <?php class myClass { ...

  3. 【SPL标准库专题(1)】 SPL简介

    什么是SPL SPL是Standard PHP Library(PHP标准库)的缩写. 根据官方定义,它是"a collection of interfaces and classes th ...

  4. php spl标准库简介(SPL是Standard PHP Library(PHP标准库)(直接看代码实例,特别方便)

    php spl标准库简介(SPL是Standard PHP Library(PHP标准库)(直接看代码实例,特别方便) 一.总结 直接看代码实例,特别方便易懂 thinkphp控制器利眠宁不支持(说明 ...

  5. PHP的SPL标准库里面的堆(SplHeap)怎么使用

    PHP的SPL标准库里面的堆(SplHeap)怎么使用 一.总结 1.因为SplHeap是抽象类,所以要先继承,实现里面的抽象方法compare后,才能new对象使用. 二.PHP的SPL标准库里面的 ...

  6. Z.ExtensionMethods 一个强大的开源扩展库

    今天有意的在博客园里面搜索了一下 Z.ExtensionMethods 这个扩展类库,确发现只搜到跟这个真正相关的才两篇博文而已,我都点进去看了一下,也都只是提到而已,没有专门介绍,才引起我写这篇文档 ...

  7. PHP使用mysqli扩展库实现增删改查(面向对象版)

    mysqli扩展库是mysql扩展库的改进版本,在mysql扩展库的基础上提高了稳定性和效率,mysqli扩展库有两套东西,一套就是面向过程的mysqli另一套是面向对象的mysqli.操作方式大体和 ...

  8. PHP mysqli 扩展库(面向对象/数据库操作封装/事务控制/预编译)

    1.和mysql扩展库的区别: (1   安全性.稳定性更高 (2  提供了面向对象和面向过程两种风格 2.php.ini  中的  extension=php_mysqli.dll 解除封印 3.面 ...

  9. PHP基础Mysql扩展库

    mysql扩展库操作步骤如下: 1.连接数据库 2.选择数据库 3.设置操作编码 4.发送指令sql,并返回结果集     ddl:数据定义语句     dml:数据操作语句     dql:数据查询 ...

随机推荐

  1. snoop的基本用法

    关于Snoop的用法   snoop是开发wpf应用程序的利器.用它可以观察WPF的可视树,监听事件,更改元素属性等.   下面我介绍下snoop一些用法.   1.获取指定应用程序的UI     打 ...

  2. springboot中@Mapper和@Repository的区别

    @Mapper和@Repository是常用的两个注解,两者都是用在dao上,两者功能差不多,容易混淆,有必要清楚其细微区别: 区别: @Repository需要在Spring中配置扫描地址,然后生成 ...

  3. 学习Java的9张思维导图

    转自:https://blog.csdn.net/aitaozi11/article/details/79652943 网上搜集了java的学习思维导图,分享给大家. 01.Java程序设计(基础) ...

  4. SpringBoot数据访问之整合mybatis注解版

    SpringBoot数据访问之整合mybatis注解版 mybatis注解版: 贴心链接:Github 在网页下方,找到快速开始文档 上述链接方便读者查找. 通过快速开始文档,搭建环境: 创建数据库: ...

  5. playwright-python 截图、录制视频、录制接口(二)

    截图 fullPage为True时,截取全屏,默认为False clip截取特定部分的图片,{"x": float, "y": float, "wid ...

  6. Vue 组件通信方案

    父组件--> 子组件 1. 属性设置 父组件关键代码如下: <template> <Child :child-msg="msg"></Child ...

  7. maven下载出错

    求解

  8. 实例_ Java中的代理模式

    静态代理 我们定义一个接口,并且使用代理模式,想要做到的就是在调用这个接口的实现类时在此方法上添加功能. public interface HelloInterface { void sayHello ...

  9. 集合框架3-Arrays 类

    Arrays 和 Collections是分别操作数组和集合的两个工具类.今天就来对 Arrays 中的内容作个总结. 一.Arrays 类概述 Arrays 类位于 java.util包中.Arra ...

  10. github上使用C语言实现的线程池

    网上介绍线程池的知识很多,但是在代码实现上介绍的又不是那么多.而且给人的一种感觉就是:你的这种实现是正规的方式还是你自己的实现? 如果有这么个疑问,且想找一个靠谱的代码拿来使用,那么这个项目是个不错的 ...