PHP学习笔记3:其它类型和类型声明
图源:php.net
Iterable 可迭代对象
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);
转化前的变量会变成stdClass 实例的scalar 属性。特别的,对于数组,会用key作为属性名,value作为值:
$student = array("name"=>"Li Lei","age"=>20);
$obj = (object)$student;
var_dump($obj);
这和将对象转化为数组的方式是相反的。
Enum 枚举
枚举功能是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;
}
因为同样的原因,也可以通过instanceof 检测:
if ($color1 instanceof Color){
echo "instace of Color".PHP_EOL;
}
枚举成员有一个name 属性,值为枚举成员名称的字符串形式:
var_dump($color1->name);
这对调试代码会有所帮助。
关于更多枚举相关的内容,可以阅读官方文档枚举。
资源类型
资源 resource 是一种特殊的类型,表示到外部资源的一个引用。
资源最大的用途是其相关的打开的文件、数据库连接、图形画布等,所以将其它类型转换为资源是没有意义的。
php解释器使用zend引擎,通过计数引用来追踪资源的使用情况,所以可以在必要的时候通过垃圾回收来关闭无效资源,所以可以不用手动关闭资源。但持久化的数据库连接比较特殊,它们不会被垃圾回收。
更多的资源类型内容可以阅读官方手册资源类型列表。
NULL
NULL类型只有一个值,就是null ,以前几种情况的变量其值是null :
函数is_null 可以检测变量的值是否为null ,其效果等同于$val === null :
$unsetVarl;
if (is_null($unsetVarl)){
echo "is_null test access".PHP_EOL;
}
if ($unsetVarl === null){
echo "=== null test access".PHP_EOL;
}
但这两者在执行效率方面在不同的PHP版本有不同的差异,简单来说,在某些早期版本is_null() 的执行效率要低于=== 运算符,但在php7以后似乎两者已经差别不大,具体可以参考官方文档is_null的用户笔记部分。
需要注意的是,不能使用== null 来检测是否为null ,因为很多0值在宽松相等检测时都是true :
if (0 == null){
echo "0 == null test access".PHP_EOL;
}
Callback/Callable 类型
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;
与很多支持函数式编程的语言(比如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]);
传递对象方法的方式是通过一个数组传递对象和方法名称:
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"));
就像上面展示的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();
同样是以数组形式传递,索引0是类名称,索引1是静态方法名称。
除了上面的一般形式以外,还支持另一种方式:
$std2 = call_func("Student::new_student");
$std2->print();
这和直接调用类静态方法的写法很相似。
官方手册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();
$std3 = call_func(array("Student2", "parent::new_student"));
$std3->print();
但就像上面展示的那样,实际在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);
这个例子中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;
这种方式的写法与Python或Go等全面支持函数式编程的语言的写法颇为类似。
关于匿名函数的更多内容可以阅读官方手册匿名函数。
类型声明
中所周知,php是一门弱类型语言,其它的弱类型语言还有Python、JavaScript等。弱类型语言的最大特点是不用声明变量类型,这在某些时候会很方便。
可能Java等强类型语言的程序员会嗤之以鼻,但老实说,就我多年的编程经验来看,强类型语言的编程中相当一部分棘手的问题是各种类型转换,在这上边的编程和debug会占用相当一部分时间,所以弱化类型是可以的的确确带来编码的效率提升上,并非是单单看上去的降低编程学习门槛那么简单。
但就像其他的语言特性那样,在某一方面有优势就意味着另一方面有缺陷,或者更中二点可以叫做“编程领域的等价交换原则”。
弱类型带来的缺陷是IDE无法通过“智能关联”来进行类型提示,更致命的是某些bug本可以通过类型检查在编码阶段发现,但弱类型语言只能通过实际执行才可以发现。
Python对此的解决方式是加入"类型注解",这是个很有意思的解决方案。类型注解本身并不会影响代码的实际执行效果,它只是起到辅助的帮助IDE识别类型的功能。
php的解决方法是为函数的参数和返回值添加上类型声明(7.4.0开始类属性也可以使用),一旦传入的参数或返回的类型不匹配,就会产生一个TypeError 异常。
这样做的好处是解决了上面提到的缺陷,但同样降低了弱类型带来的灵活性。但优点在于,是否要添加类型声明取决于开发者,所以这并不僵化。
可以作为类型声明的有:
- 类/接口名称
- array
- callable
- bool
- float
- int
- string
- iterable
- object
- mixed
此外还有self 和parent 可以在类中使用,前者用于声明是一个当前类的实例,后者用于声明是父类的一个实例。但我认为这并非必须,完全可以使用当前类或父类的名称来进行类型声明。
mixed 相当于Go语言的interface{} ,可以代表任意一种类型。
mixed 相当于联合类型object|resource|array|string|int|float|bool|null ,php8.0起可用。
Nullable类型
需要注意的是,在强类型语言中,某个变量的类型是某个类,则该变量是可以接受null 值的。但php的类型声明并不符合这个特点:
class Student{}
Class Teacher{}
function printStudent(Student $student){
;
}
printStudent(new Student);
printStudent(null);
错误提示说的很明确,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){
;
}
false 伪类型
因为历史原因,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));
var_dump(findStudent("Xiao Hong", $students));
作为“伪类型”,false必须与其它真实类型联合使用,不能单独进行使用,也不能和null等其他伪类型一起使用。
冗余类型
如果联合类型中的子类型存在冗余或冲突的情况,是不被允许的:
function do_something(bool|false $param){
;
}
这有助于我们编写错误的联合类型。但某些情况是无法通过静态检查检测出来的:
class Person{};
class Student extends Person{};
function do_something(Student|Person $person){
;
}
Student|Person 在这里显得相当多余,因为Student 类型本身就是Person 类型,所以完全可以用Person 来代替。但这种继承关系很难通过静态检查发现,需要实际运行时加载类定义。而类型声明的价值就是编码时的静态检查,所以php对这种问题是允许的。
特殊类型
void
void 表示函数没有返回值:
function no_return():void{
return 11;
}
在一个void 函数中返回数据会产生错误。
void 不能用于联合类型。
never
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));
var_dump(add(1.5,2.6));
可以通过declare 开启严格类型:
declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
var_dump(add(1,2));
var_dump(add(1.5,2.6));
此时只有类型完全一致才能正常执行。
以上就是本篇笔记的全部内容,谢谢阅读。
本系列所有文章的相关代码都收录在php-notes。
往期内容
|