图源:php.net
Iterable是php的一个伪类型,包含数组或者实现了Traversable接口的对象。Iterable类型可以被foreach
迭代,也可以和生成器相关的yield from
语句一起使用。
Iterable
可以用于参数类型约束,且进一步通过foreach
语句迭代:
function do_something(iterable $iter){
foreach($iter as $item){
...
}
}
可以使用null
或空数组作为iterable
类型参数的默认值:
function do_something(iterable $iter = [])
{
foreach ($iter as $item) {
...
}
}
iterable
类型也可以作为返回值:
function do_something(): iterable
{
return [1, 2, 3];
}
生成器函数的返回值也可以声明为iterable
类型:
function gen(): iterable
{
yield 1;
yield 2;
yield 3;
}
可以使用new
关键字创建对象:
class Student
{
private $name;
private $age;
function __construct()
{
$name = "";
$age = 0;
}
}
$std = new Student;
尝试将其它类型的变量转化为对象,会产生一个内置的stdClass
类的实例:
$number = 10;
var_dump((object)$number);
// object(stdClass)#1 (1) {
// ["scalar"]=>
// int(10)
// }
转化前的变量会变成stdClass
实例的scalar
属性。特别的,对于数组,会用key作为属性名,value作为值:
$student = array("name"=>"Li Lei","age"=>20);
$obj = (object)$student;
var_dump($obj);
// object(stdClass)#1 (2) {
// ["name"]=>
// string(6) "Li Lei"
// ["age"]=>
// int(20)
// }
这和将对象转化为数组的方式是相反的。
枚举功能是php8.1.0加入的,虽然没有枚举时也可以用类静态常量来替代,但在某些方面依然不如完善的枚举功能好用。
php的枚举类型是一种特殊的类,可以使用case
创建对应的枚举成员,其实质是该类的单例。所以枚举是一个完备的封闭集合类型。这点和其它语言(如Python)中的枚举类型是一致的。
枚举类型的定义:
enum Color
{
case Red;
case Blue;
case Yellow;
case Green;
}
需要注意的是,8.1.0版本刚刚发布,相关工具的支持都很不完备,所以以上代码在VSC中依然会提示语法错误,但修改首选项中的PHP相关设置,将语法级别修改为8.1.0后,虽然会提示语法错误,但依然可以正确执行代码。
枚举类型可以作为函数参数:
function do_something(Color $color){
if ($color == Color::Red){
echo "good color!".PHP_EOL;
}
else{
echo "bad color.".PHP_EOL;
}
}
do_something(Color::Blue);
do_something(Color::Yellow);
do_something(Color::Red);
因为枚举成员实质上是枚举类的单例,所以可以使用===
进行比较:
$color1 = Color::Blue;
if ($color1 === Color::Blue){
echo "equal!".PHP_EOL;
// equal!
}
因为同样的原因,也可以通过instanceof
检测:
if ($color1 instanceof Color){
echo "instace of Color".PHP_EOL;
// instace of Color
}
枚举成员有一个name
属性,值为枚举成员名称的字符串形式:
var_dump($color1->name);
// string(4) "Blue"
这对调试代码会有所帮助。
关于更多枚举相关的内容,可以阅读官方文档枚举。
资源 resource
是一种特殊的类型,表示到外部资源的一个引用。
资源最大的用途是其相关的打开的文件、数据库连接、图形画布等,所以将其它类型转换为资源是没有意义的。
php解释器使用zend引擎,通过计数引用来追踪资源的使用情况,所以可以在必要的时候通过垃圾回收来关闭无效资源,所以可以不用手动关闭资源。但持久化的数据库连接比较特殊,它们不会被垃圾回收。
更多的资源类型内容可以阅读官方手册资源类型列表。
NULL类型只有一个值,就是null
,以前几种情况的变量其值是null
:
null
unset
销毁函数is_null
可以检测变量的值是否为null
,其效果等同于$val === null
:
$unsetVarl;
if (is_null($unsetVarl)){
echo "is_null test access".PHP_EOL;
// is_null test access
}
if ($unsetVarl === null){
echo "=== null test access".PHP_EOL;
// === null test access
}
但这两者在执行效率方面在不同的PHP版本有不同的差异,简单来说,在某些早期版本is_null()
的执行效率要低于===
运算符,但在php7以后似乎两者已经差别不大,具体可以参考官方文档is_null的用户笔记部分。
需要注意的是,不能使用== null
来检测是否为null
,因为很多0值在宽松相等检测时都是true
:
if (0 == null){
echo "0 == null test access".PHP_EOL;
// 0 == null test access
}
Callable
类型可以表示可调用的类型,这在支持函数式编程的语言中非常普遍(如Python或Go)。在php中,具体包含内建函数、用户自定义函数、对象方法、类方法、匿名函数、实现了__invoke()
方法的对象等。
可以定义Callable
类型的参数或返回值:
function call_func(callable $func, ...$param)
{
return $func(...$param);
}
示例函数call_func
接受一个Callable
类型的参数$func
,以及一个边长参数列表$param
,然后调用$func
并传入可变参数,并返回结果。
要向一个参数是callable
类型的函数传入参数,试不同的Callable
类型有不同的方式,对于内建函数,可以直接传入字符串形式的函数名:
$arr = ["a","b","c"];
echo call_func('implode',',',$arr).PHP_EOL;
// a,b,c
与很多支持函数式编程的语言(比如Python或Go),这样的写法可读性很差,但这是有原因的。因为call_func(implode,','.$arr)
这样的写法在php中是非法的,因为implode
会被解析成一个未定义的常量。
传递自定义函数的方式与内建函数相同:
function my_print(...$params)
{
foreach ($params as $param) {
echo $param . " ";
}
echo PHP_EOL;
}
call_func("my_print", ...[1, 2, 3]);
// 1 2 3
传递对象方法的方式是通过一个数组传递对象和方法名称:
class Student
{
private $name;
private $age;
function __construct($name, $age)
{
$this->name = $name;
$this->age = $age;
}
function print()
{
echo "name:{$this->name},age:{$this->age}" . PHP_EOL;
}
}
$std = new Student("Li Lei", 20);
call_func(array($std, "print"));
// name:Li Lei,age:20
就像上面展示的call_func(array($std, "print"));
,对象位于数组的索引0,包含方法名称的字符串位于索引1。
尝试在类定义之外传递并调用一个private
或protected
会产生一个错误,内容类似Fatal error: Uncaught TypeError: call_func(): Argument #1 ($func) must be of type callable
。
所以这种传递函数并调用的机制依然符合类封装的相关约束。
传递类静态方法:
class Student
{
...
public static function new_student()
{
return new Student("", 0);
}
}
$std2 = call_func(array("Student","new_student"));
$std2->print();
// name:,age:0
同样是以数组形式传递,索引0是类名称,索引1是静态方法名称。
除了上面的一般形式以外,还支持另一种方式:
$std2 = call_func("Student::new_student");
$std2->print();
// name:,age:0
这和直接调用类静态方法的写法很相似。
官方手册Callback / Callable 类型还介绍了可以传递并调用某个类父类的指定方法:
class Student2 extends Student
{
public static function new_student()
{
return new Student2("new student", 20);
}
}
$std3 = call_func(array("Student2", "new_student"));
$std3->print();
// name:new student,age:20
$std3 = call_func(array("Student2", "parent::new_student"));
$std3->print();
// Fatal error: Uncaught Error: Call to undefined method Student2::parent::new_student() in ...
但就像上面展示的那样,实际在php8.1.0下执行会显示Student2::parent::new_student()
方法没有被定义,不知道这是一个bug还是什么。
传递一个实现了__invoke
方法的对象:
class Calculator
{
private $operator = [];
private $inputData = [];
private $result = 0;
private $history = [];
private function do_calculate()
{
$this->result = 0;
$this->hitory = [];
$operatorLen = count($this->operator);
$inputDataLen = count($this->inputData);
if ($operatorLen != $inputDataLen) {
return;
}
if ($operatorLen <= 0) {
return;
}
$this->history[] = "0";
foreach ($this->inputData as $index => $data) {
$opt = $this->operator[$index];
if ($opt == '+') {
$this->result += $data;
$this->history[] = "+{$data}";
} else if ($opt == '-') {
$this->result -= $data;
$this->history[] = "-{$data}";
} else {;
}
}
$this->history[] = "={$this->result}";
}
public function add(int $num)
{
$this->operator[] = '+';
$this->inputData[] = $num;
}
public function sub($num)
{
$this->operator[] = '-';
$this->inputData[] = $num;
}
public function __invoke()
{
$this->do_calculate();
echo implode('',$this->history).PHP_EOL;
}
}
$cal = new Calculator;
$cal->add(5);
$cal->add(10);
$cal->sub(5);
$cal->add(7);
$cal->sub(20);
call_func($cal);
// 0+5+10-5+7-20=-3
这个例子中Calculator
是一个可以延迟计算的计算器,通过add
和sub
方法可以给计算器添加加运算或减运算,__invoke
方法负责最终的计算并打印结果。可以看到将$cal
对象传递给call_func
后,的确输出了计算过程和结果。
传递一个匿名函数(闭包):
$nonNamedFunc = function (int $a, int $b): int {
return $a + $b;
};
$result = call_func($nonNamedFunc, 1, 6);
echo $result.PHP_EOL;
// 7
这种方式的写法与Python或Go等全面支持函数式编程的语言的写法颇为类似。
关于匿名函数的更多内容可以阅读官方手册匿名函数。
中所周知,php是一门弱类型语言,其它的弱类型语言还有Python、JavaScript等。弱类型语言的最大特点是不用声明变量类型,这在某些时候会很方便。
可能Java等强类型语言的程序员会嗤之以鼻,但老实说,就我多年的编程经验来看,强类型语言的编程中相当一部分棘手的问题是各种类型转换,在这上边的编程和debug会占用相当一部分时间,所以弱化类型是可以的的确确带来编码的效率提升上,并非是单单看上去的降低编程学习门槛那么简单。
但就像其他的语言特性那样,在某一方面有优势就意味着另一方面有缺陷,或者更中二点可以叫做“编程领域的等价交换原则”。
弱类型带来的缺陷是IDE无法通过“智能关联”来进行类型提示,更致命的是某些bug本可以通过类型检查在编码阶段发现,但弱类型语言只能通过实际执行才可以发现。
Python对此的解决方式是加入"类型注解",这是个很有意思的解决方案。类型注解本身并不会影响代码的实际执行效果,它只是起到辅助的帮助IDE识别类型的功能。
php的解决方法是为函数的参数和返回值添加上类型声明(7.4.0开始类属性也可以使用),一旦传入的参数或返回的类型不匹配,就会产生一个TypeError
异常。
这样做的好处是解决了上面提到的缺陷,但同样降低了弱类型带来的灵活性。但优点在于,是否要添加类型声明取决于开发者,所以这并不僵化。
可以作为类型声明的有:
此外还有self
和parent
可以在类中使用,前者用于声明是一个当前类的实例,后者用于声明是父类的一个实例。但我认为这并非必须,完全可以使用当前类或父类的名称来进行类型声明。
mixed
相当于Go语言的interface{}
,可以代表任意一种类型。
mixed
相当于联合类型object|resource|array|string|int|float|bool|null
,php8.0起可用。
需要注意的是,在强类型语言中,某个变量的类型是某个类,则该变量是可以接受null
值的。但php的类型声明并不符合这个特点:
class Student{}
Class Teacher{}
function printStudent(Student $student){
;
}
printStudent(new Student);
printStudent(null);
// Fatal error: Uncaught TypeError: printStudent(): Argument #1 ($student) must be of type Student, null given,
错误提示说的很明确,printStudent
必须传入一个Student
类型作为参数,但这里传入的是null
,所以产生一个TypeError
类型的错误。
要解决这个问题,需要使用Nullable类型,其写法是在自定义类型前加上?
,这表示这个类型可以是null
:
function printStudent(?Student $student){
;
}
- Nullable类型从7.1.0起可用。
null
并不能作为一个类型单独使用。
联合类型从php8.0.0起可用,其语法是T1|T2|T3|...
,Nullable类型可以看做是和null|T
等效:
function printStudent(null|Student $student){
;
}
因为历史原因,php中很多函数都是在失败时直接返回false
(以前我也经常这么做),对于这样的函数,可以将false
作为一个伪类型与其它类型组成联合类型:
class Student
{
public $name;
public $age;
public function __construct($name, $age)
{
$this->name = $name;
$this->age = $age;
}
}
function findStudent(string $name, array $students): false|Student
{
foreach ($students as $student) {
if ($student->name == $name) {
return $student;
}
}
return false;
}
$students = [new Student("Li Lei", 20), new Student("Han Mei", 15), new Student("Xiao Li", 20)];
var_dump(findStudent("Xiao Li", $students));
// object(Student)#3 (2) {
// ["name"]=>
// string(7) "Xiao Li"
// ["age"]=>
// int(20)
// }
var_dump(findStudent("Xiao Hong", $students));
// bool(false)
作为“伪类型”,false必须与其它真实类型联合使用,不能单独进行使用,也不能和null等其他伪类型一起使用。
如果联合类型中的子类型存在冗余或冲突的情况,是不被允许的:
function do_something(bool|false $param){
;
}
// Fatal error: Duplicate type false is redundant
这有助于我们编写错误的联合类型。但某些情况是无法通过静态检查检测出来的:
class Person{};
class Student extends Person{};
function do_something(Student|Person $person){
;
}
Student|Person
在这里显得相当多余,因为Student
类型本身就是Person
类型,所以完全可以用Person
来代替。但这种继承关系很难通过静态检查发现,需要实际运行时加载类定义。而类型声明的价值就是编码时的静态检查,所以php对这种问题是允许的。
void
表示函数没有返回值:
function no_return():void{
return 11;
}
// Fatal error: A void function must not return a value
在一个void
函数中返回数据会产生错误。
void
不能用于联合类型。
never
表示函数“不会返回”。不会返回和没有返回值是有区别的,前者意味着函数中可能存在exit()
调用导致程序退出,或者无限循环:
function never_func(): never
{
while (true) {
sleep(1);
}
}
never_func();
never
从php8.1.0起可用,不能被用于联合类型。
默认情况下,当传入参数的类型与形参声明的类型并不完全一致时,会尝试进行转换:
function add(int $a, int $b): int
{
return $a + $b;
}
var_dump(add(1,2));
// int(3)
var_dump(add(1.5,2.6));
// int(3)
可以通过declare
开启严格类型:
declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
var_dump(add(1,2));
// int(3)
var_dump(add(1.5,2.6));
// Fatal error: Uncaught TypeError: add(): Argument #1 ($a) must be of type int, float given,
此时只有类型完全一致才能正常执行。
以上就是本篇笔记的全部内容,谢谢阅读。
本系列所有文章的相关代码都收录在php-notes。