类型化类属性已在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
PHP 7.4中的类型化属性