PHP7.4中增加了类型化类属性,对php的类型系统进行了重大改进。这些更改完全是自愿加入的,不会破坏以前的版本。
在本文中,我们将深入了解该功能,但首先让我们总结一下最重要的几点:
这些更改自PHP7.4起可用,计划于年11月发布它们仅在类中可用,并且需要访问修饰符:public、protected或private;或var允许所有类型,但void和callable除外他们的实际情况是这样的:
classFoo{publicint$a;public?string$b=foo;privateFoo$prop;protectedstaticstring$static=default;}如果你不确定类型的额外好处,我建议您首先阅读这篇文章。
未初始化
在进入正题之前,首先要探讨一个与类型属性有关的重要方面。
不管你第一眼看到这段代码是怎么想的,但它的确是合法的
classFoo{publicint$bar;}$foo=newFoo;即便是类的实例化后$bar值仍不是整数值的情况下,PHP也只是会在访问$bar时才会报错:
var_dump($foo-bar);Fatalerror:UncaughtError:TypedpropertyFoo::$barmustnotbeaccessedbeforeinitialization从错误消息中可以看出,出现了一种新的变量状态:未初始化(uninitialized)
$bar属性无论是否声明了类型,值都可以为null。因此,无法确定类型属性是否设置。这就是增加变量「未初始化」状态的原因。
未初始化有四个方面需要注意:
无法读取未初始化的属性,一旦这么做,将引发致命错误;由于在访问属性时会检查未初始化状态,所以即使是不可为空的对象也可以使用未初始化属性;在读取未初始化属性时候之前可以对其进行写入;unset操作会让类型属性变成未初始化状态,而非类型属性只会变成值为null;特别要注意在对象实例化之后设置未初始化的类型属性是合法的:
classFoo{publicint$a;}$foo=newFoo;$foo-a=1;//合法$foo-a=null;//非法虽然只会在读取属性值时检查未初始化状态,但在写入属性时会进行类型验证。这意味着任何无效的属性值都不会被设置。
默认值和构造函数
让我们仔细看看如何初始化类型属性值。对于标量类型,可以直接提供一个默认值
classFoo{publicint$bar=4;public?string$baz=null;//错误写法publicstring$baz=null;publicarray$list=[1,2,3];}类型属性不能显示设置为null,除非是可空类型。这看上去显而易见的,但是一些旧行为却允许这种操作
functionpassNull(int$i=null){/*…*/}passNull(null);幸运的是,类型属性不允许这种令人疑惑的行为。还要注意,属性类型的默认值不可能为对象或者类,你应当使用构造器来设置这些值。
最明显的用来设置默认值的地方就是构造函数
classFoo{privateint$a;publicfunction__construct(int$a){$this-a=$a;}}但也要记住我之前提到的内容:在构造函数之外写入未初始化(uninitialized)的属性是有效的。只要没有读取属性值的操作,编译器就不会执行未初始化的相关检查。
类型
那么到底哪些类型可以指定,又如何指定呢?我已经提到了指定属性类型只能在类中进行(当前如此),并且它们需要一个访问修饰符或是属性前面的var关键字。
对于可用类型,几乎所有类型都可以使用,除了void和callable类型.
因为void意味着没有值,所以它不能用于指定一个值的类型也就说得过去了。然而callback就有一点细微不同了。
可见,PHP中的callback可以这样写
$callable=[$this,method];假设你有以下(无效)代码:
classFoo{publiccallable$callable;publicfunction__construct(callable$callable){/*…*/}}classBar{publicFoo$foo;publicfunction__construct(){$this-foo=newFoo([$this,method])}privatefunctionmethod(){/*…*/}}$bar=newBar;($bar-foo-callable)();在此例中,$callback引用了私有的Bar::method,但是是在Foo的上下文中被调用的。基于这个问题,决定不添加callback类型的支持。
不过这并不是什么大问题,因为Closure(闭包)是一种有效类型,它会记住构建它的$this上下文。
顺带一说,以下是所有可用类型的列表:
boolintfloatstringarrayiterableobject?(nullable)selfparentClassesinterfaces强制和严格类型
PHP,是我们既喜欢又反感的动态语言,它会尽可能地强制或转换类型。假设你在一个期望接受int的地方传入字符串,PHP会试着自动转换该字符串:
functioncoerce(int$i){/*…*/}coerce(1);//1同样的原则也适用于已指定类型的属性,下面的代码是有效的,且会将1转换为1.
classBar{publicint$i;}$bar=newBar;$bar-i=1;//1如果你并不喜欢这种(自动转换)行为,可以通过声明严格类型来禁用它:
declare(strict_types=1);$bar=newBar;$bar-i=1;//1Fatalerror:UncaughtTypeError:TypedpropertyBar::$imustbeint,stringused类型差异和继承
即使PHP7.4引入了改进的类型差异,但是类型的属性仍然是不变的。这意味着以下写法是无效的:
classA{}classBextendsA{}classFoo{publicA$prop;}classBarextendsFoo{publicB$prop;}Fatalerror:TypeofBar::$propmustbeA(asinclassFoo)如果上面的示例看起来不够明显的话,你可以查看以下内容:
classFoo{publicself$prop;}classBarextendsFoo{publicself$prop;}在运行代码之前,PHP将在背后用它所引用的具体实现类来替换self。这意味着在此本例中将抛出相同的错误。解决此问题的唯一方法是执行以下操作:
classFoo{publicFoo$prop;}classBarextendsFoo{publicFoo$prop;}谈到继承,您可能会发现很难想出任何好的用例来重写继承属性的类型。
尽管我同意这种观点,但值得注意的是更改继承属性的类型是可能实现的,前提是访问修饰符也必须从private更改为protected或public。
以下代码是有效的:
classFoo{privateint$prop;}classBarextendsFoo{publicstring$prop;}但是,从可空的类型改为不可空或反向的类型是不允许的。
classFoo{publicint$a;public?int$b;}classBarextendsFoo{public?int$a;publicint$b;}Fatalerror:TypeofBar::$amustbeint(asinclassFoo)还有更多!
正如开头所说,类型化属性是PHP的主要补充。关于它们更多的内容,我建议您通读RFC以了解所有细节。
如果您不熟悉PHP7.4,则可能需要阅读完整列表中所做的更改和添加的功能。老实说,这是很长一段时间以来最好的发行版之一,值得您花时间!