sj-i/typist

强制局部变量的类型

v1.0.0 2020-07-26 15:22 UTC

This package is auto-updated.

Last update: 2024-09-15 17:52:06 UTC


README

Minimum PHP version: 7.4.0 Packagist Packagist Downloads Github Actions Scrutinizer Code Quality Coverage Status Psalm coverage

Typist 是一个 PHP 库,用于强制局部变量的类型。
它内部使用 PHP 7.4 中引入的带有类型的属性的引用。

安装

composer require sj-i/typist

支持的版本

  • PHP 7.4 或更高版本

用法

基本用法

use Typist\Typist;

// type enforcements are valid during the lifetime of this `$_`
$_ = [
    Typist::int($typed_int, 1),
    Typist::string($typed_string, 'str'),
    Typist::bool($typed_bool, false),
    Typist::float($typed_float, 0.1),
    Typist::class(\DateTimeInterface::class, $typed_object, new \DateTime()),
];

assert($typed_int === 1);
assert($typed_string === 'str');
assert($typed_bool === false);
assert($typed_float === 0.1);
assert($typed_object instanceof \DateTime);

// modifications with valid types are OK
$typed_int = 2;
$typed_string = 'trs';
$typed_bool = true;
$typed_float = -0.1;
$typed_object = new DateTimeImmutable();

// any statements below raises TypeError
$typed_int = 'a';
$typed_string = 1;
$typed_bool = 'a';
$typed_float = 'a';
$typed_object = 'a';

还提供了函数接口。

use function Typist\int;
use function Typist\float;
use function Typist\string;
use function Typist\bool;
use function Typist\class_; // trailing underscore is needed

$_ = [
    int($typed_int, 1),
    string($typed_string, 'str'),
    bool($typed_bool, false),
    float($typed_float, 0.1),
    class_(\DateTimeInterface::class, $typed_object, new \DateTime()),
];

可空类型

use Typist\Typist;

$_ = [
    Typist::nullable()::int($typed_int1, 1),
    Typist::nullable()::int($typed_int2, null),
    Typist::nullable()::string($typed_string1, 'str'),
    Typist::nullable()::string($typed_string2, null),
    Typist::nullable()::bool($typed_bool1, false),
    Typist::nullable()::bool($typed_bool2, null),
    Typist::nullable()::float($typed_float1, 0.1),
    Typist::nullable()::float($typed_float2, null),
    Typist::nullable()::class(\DateTimeInterface::class, $typed_object1, new \DateTime()),
    Typist::nullable()::class(\DateTimeInterface::class, $typed_object2, null),
];

或者如果你使用 PHP8,() 可以省略。

use Typist\Typist;

$_ = [
    Typist::nullable::int($typed_int, null),
    Typist::nullable::string($typed_string, null),
    Typist::nullable::bool($typed_bool, null),
    Typist::nullable::float($typed_float, null),
    Typist::nullable::class(\DateTimeInterface::class, $typed_object, null),
];

这里也提供了函数接口。

use function Typist\nullable_int;
use function Typist\nullable_float;
use function Typist\nullable_string;
use function Typist\nullable_bool;
use function Typist\nullable_class; // trailing underscore is not needed

$_ = [
    nullable_int($typed_int, null),
    nullable_string($typed_string, null),
    nullable_bool($typed_bool, null),
    nullable_float($typed_float, null),
    nullable_class(\DateTimeInterface::class, $typed_object, null),
];

工作原理

如果你曾经阅读过带有类型的属性的 RFC,特别是描述其如何与引用一起工作的部分,你可能不会有任何不清楚的地方。

因此,我试图在这里加入一些解释,考虑到那些还没有阅读的读者。

PHP 中的引用

首先,请看以下代码。

$a = 1;
$b =& $a;
$c =& $b;

让我们一步一步地展示这段代码,逐行解释。

第一行

$a = 1;

第一行可以表示如下。

php_reference_a

有一个名为 $a 的变量,其值为 1
就这样,非常简单。

第二行

$b =& $a;

然后代码添加了第二行,可以表示如下。

php_reference_ab

有两个名为 $a$b 的变量。
它们只是不同名称的相同数据,在这种情况下是 1。它们之间的关系不是“$a 是一个包含 1 的变量,$b 是指向 $a 的指针”。引用赋值运算符 =& 的功能只是创建数据的另一个名称。

第三行

$c =& $b;

另一个引用赋值创建了一个新的名称。就是这样。最终状态可以表示如下。

php_reference_abc

因此,可以有一个由多个名称组成的组,这些名称表示相同的数据。通过组中的任何名称进行的任何修改都会影响通过组中的任何其他名称读取的结果。带有类型的属性的 RFC 将它们称为“引用集”。

带有类型的属性和引用

那么,如果一个引用集包含带有类型的属性会发生什么?

php_reference_abc_typed

现在涉及到属性了,所以使用 $a$b 作为例子不再合适。这次我将代码改为以下内容。

$o = new class() {
    public int $a = 1;
    public float $b;
};
$o->b =& $o->a;
$c =& $o->b;

它可以重新表示如下。

php_reference_typed_properties_ab_local_variable_c

那么让我们尝试给 $o->a 赋值。

$o->a = 1; // legal
$o->a = 'abc'; // TypeError will be thrown. $o->a is declared as int

这是简单易懂的结果。

另一方面,下面这个例子呢?

$o->b = 1.5; // $o->b is declared as float, but...
var_dump($o->a); // $o->a is declared as int, so this must not be `1.5`!

再看看下面这个例子呢?

$c = new DateTime(); // $c is a local variable, so doesn't have any type constraint itself, but...
var_dump($o->a, $o->b); // Neither $o->a and $o->b must not be a DateTime!!!

如果一个引用集包含带有类型的属性,该集中的所有类型在赋值时都必须得到满足。解释器实际上会检查该情况下的每个属性类型,并在出错时抛出 \TypeError

在这里,你已经看到了局部变量如何成为带有类型的变量。带有类型的属性的引用集内的局部变量会根据其类型进行检查。

你可以通过两种方式获得这样的局部变量。一种是从带有类型的属性进行引用赋值,另一种是引用赋值到带有类型的属性。

从或到带有类型的属性的引用赋值,返回引用或按引用传递

从带有类型的属性进行引用赋值可以创建带有类型的局部变量。虽然我没有选择这种方式,但我认为这种方法直观,因为我们实际上是在尝试提取带有类型的属性中的类型检查能力。

我看到了两种这种方法的情况。

它们的实现可以总结如下。

  • 为每种类型定义如下函数。
    • 实例化具有请求类型的属性的对象。
    • 从全局状态中获取对象,因为否则对象将被垃圾回收。
    • 返回类型属性的引用。

两者都会泄漏内存,因为每次创建类型变量都会将类型属性对象的引用放入全局状态。azjezz/typed提供了手动释放引用的方法

我对这个方法有两点不喜欢。

  1. 内存泄漏(或手动内存管理)
  2. 在用户代码中强制引用分配
    • 到处使用 &= 有点不美观!
    • 是的,这只是我的个人喜好!

如你所见,从本仓库的代码中可以看出,问题2可以通过使用引用传递来解决。
从类型属性进行引用分配将创建一个包含类型属性的引用集。
将引用分配到类型属性也会创建一个包含类型属性的引用集。

然后可以使用返回值做其他事情。
通过返回类型属性对象(在这个库中称为Enforcer)并在调用者中通过局部变量获取它,可以解决问题1。这个变量仅用于保持Enforcer的引用计数,所以一个好的不明显名称如 $_ 会很好。

与psalm一起使用

Psalm 当传递引用变量的类型发生变化时发出警告。因此,如果你使用这个库,局部变量的类型也可以进行静态检查。此外,$_ 被UnusedVariable检查忽略