PHP学习笔记3:其它类型和类型声明

PHP学习笔记3:其它类型和类型声明

PHP学习笔记3:其它类型和类型声明_第1张图片

图源: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);
// 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)
//   }

这和将对象转化为数组的方式是相反的。

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;
    // 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

  • 被赋值为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
}

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;
// 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。

尝试在类定义之外传递并调用一个privateprotected会产生一个错误,内容类似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是一个可以延迟计算的计算器,通过addsub方法可以给计算器添加加运算或减运算,__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异常。

这样做的好处是解决了上面提到的缺陷,但同样降低了弱类型带来的灵活性。但优点在于,是否要添加类型声明取决于开发者,所以这并不僵化。

可以作为类型声明的有:

  • 类/接口名称
  • array
  • callable
  • bool
  • float
  • int
  • string
  • iterable
  • object
  • mixed

此外还有selfparent可以在类中使用,前者用于声明是一个当前类的实例,后者用于声明是父类的一个实例。但我认为这并非必须,完全可以使用当前类或父类的名称来进行类型声明。

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);
// 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){
    ;
}

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));
// 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

void表示函数没有返回值:

function no_return():void{
    return 11;
}
// Fatal error: A void function must not return a value 

在一个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));
// 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

往期内容

  • PHP学习笔记2:数组
  • PHP学习笔记1:基础

你可能感兴趣的:(PHP,开发语言,php,类型声明)