类型化类属性已在PHP 7.4中添加,并为PHP的类型系统提供了重大改进。这些更改是完全可选的,并且不破坏以前的版本。
在本文中,我们将深入研究该功能,但首先让我们首先总结最重要的要点:
- 它们自PHP 7.4起可用,该版本于2019年11月发布。
- 他们只在类 上使用,并且需要访问修饰符:
public
,protected
或private
; 或var
- 允许所有类型,除了
void
和callable
他们的实际情况是这样的:
class Foo { public int $a; public ?string $b = 'foo'; private Foo $prop; protected static string $static = 'default'; }
如果不确定类型带来的好处,建议您先阅读这篇文章。
未初始化
在看有趣的东西之前,有一个关于类型化属性的重要方面,这是必须首先讨论的。
尽管您可能会乍一看,但以下代码仍然有效:
class Foo { public int $bar; } $foo = new Foo;
即使$bar的值在创建 Foo 对象后不是整数,PHP 仅在访问$bar时才会引发错误:
var_dump($foo->bar);
Fatal error: Uncaught Error: Typed property Foo::$bar must not be accessed before initialization
从错误消息中可以看到,有一种新的“变量状态”:未初始化。
如果$bar
没有类型,则其值将为null
。但是类型可以为空,因此无法确定是否设置了类型为空的属性,或者只是将其忘记了。这就是为什么添加了“未初始化”的原因。
关于未初始化,要记住四件事:
- 您无法从未初始化的属性读取,否则将导致致命错误。
- 由于访问属性时会检查未初始化状态,因此即使其类型为不可为空,您也可以使用未初始化属性创建对象。
- 您可以先写入未初始化的属性,然后再读取它。
unset
在类型化的属性上使用会使它未初始化,而对未类型化的属性进行unset则会使它初始化null
。
特别要注意的是,以下代码在构造对象之后设置了未初始化的,不可为空的属性,这是有效的
class Foo { public int $a; } $foo = new Foo; $foo->a = 1;
尽管仅在读取属性值时才检查未初始化状态,但在写入属性时会进行类型验证。这意味着您可以确保没有任何无效类型最终会成为属性的值.
默认值和构造函数
让我们仔细看看如何初始化键入的值。对于标量类型,可以提供一个默认值:
class Foo { public int $bar = 4; public ?string $baz = null; public array $list = [1, 2, 3]; }
请注意,仅当类型实际为可为空时,才能将 null 用作默认值。这似乎是显而易见的,但允许以下情况的参数默认值存在一些遗留行为:
function passNull(int $i = null) { /* … */ } passNull(null);
幸运的是,类型属性不允许这种令人困惑的行为。
另请注意,不可能有object或class类型的默认值。应使用构造函数设置其默认值。
初始化类型化值的明显地方当然是构造函数:
class Foo { private int $a; public function __construct(int $a) { $this->a = $a; } }
但也请记住我之前提到的内容:在构造函数之外写入未初始化的属性是有效的。只要没有任何内容从属性中读取,就不会执行未初始化的检查。
类型的类型
那么究竟可以类型化什么以及如何类型化?我已经提到过,类型化属性仅在类中起作用(目前),并且它们需要访问修饰符或var
它们前面的关键字
从可用类型开始,几乎可以使用除void
和callable
之外的所有类型。
因为 void 表示缺少值,因此不能用于类型化。然而,callable是有点细微差别。
PHP中的“ callable” 可以这样写:
$callable = [$this, 'method'];
假设您有以下(无效)代码:
class Foo { public callable $callable; public function __construct(callable $callable) { /* … */ } } class Bar { public Foo $foo; public function __construct() { $this->foo = new Foo([$this, 'method']) } private function method() { /* … */ } } $bar = new Bar; ($bar->foo->callable)();
在此例中, $callable
指的是 private Bar::method
, 但是它在 Foo
环境中被调用. 因为这个问题, 它决定不添加 callable
支持.
这没什么大不了的 , 因为 Closure
是一个有效类型, 它将记住从哪里构造它的$this
上下文。
顺便说一句,这是所有可用类型的列表:
- bool
- int
- float
- string
- array
- iterable
- object
- ? (nullable)
- self & parent
- Classes & interfaces
强制和严格类型
PHP是我们喜欢和讨厌的一种动态语言,它将尽可能地强制转换类型。假设您在期望整数的地方传递了一个字符串,PHP将尝试自动转换该字符串:
function coerce(int $i) { /* … */ } coerce('1'); // 1
相同的原则适用于类型化属性。以下代码有效,并将转换'1'
为1
。
class Bar { public int $i; } $bar = new Bar; $bar->i = '1'; // 1
如果您不喜欢这种行为,可以通过声明严格类型来禁用它:
declare(strict_types=1); $bar = new Bar; $bar->i = '1'; // 1 Fatal error: Uncaught TypeError: Typed property Bar::$i must be int, string used
类型差异和继承
即使PHP 7.4引入了改进的类型差异,但类型化属性仍然不变。这意味着以下无效
class A {} class B extends A {} class Foo { public A $prop; } class Bar extends Foo { public B $prop; } Fatal error: Type of Bar::$prop must be A (as in class Foo)
如果上面的示例似乎并不有意义,则应查看以下内容:
class Foo { public self $prop; } class Bar extends Foo { public self $prop; }
self
在运行代码之前,PHP将用它所引用的具体类在后台替换它。这意味着在此示例中将引发相同的错误。处理此问题的唯一方法是执行以下操作:
class Foo { public Foo $prop; } class Bar extends Foo { public Foo $prop; }
说到继承,您可能会发现很难提出任何好的用例来覆盖继承属性的类型。
虽然我同意这种看法,但值得注意的是,可以更改继承属性的类型,但前提是访问修饰符也从 private
到 protected
或 public
.。
以下代码有效:
class Foo { private int $prop; } class Bar extends Foo { public string $prop; }
但是,不允许将类型从可为空的类型更改为不可为空或反之也是。
class Foo { public int $a; public ?int $b; } class Bar extends Foo { public ?int $a; public int $b; } Fatal error: Type of Bar::$a must be int (as in class Foo)
还有更多!
就像本文开头所说的那样,类型化属性是PHP的主要补充。关于它们还有很多要说的。我建议您阅读RFC,以了解所有整洁的小细节。
如果您不是PHP 7.4的新手,您可能想阅读所做的更改和添加的功能的完整列表。老实说,这是很长一段时间以来最好的发行版之一,值得您花时间!
来自https://stitcher.io/blog/typed-properties-in-php-74