【PHP8.3】PHP的随机数生成算法得到进一步改进

在PHP 8.2中,随机数得到了大幅改善,但很快在PHP 8.3中,决定增加了一些新功能。

以下是RFC(Randomizer Additions)的介绍。

PHP RFC: 随机数生成器 添加功能

介绍在这份RFC中,我们建议向\Random\Randomizer中添加一些在用户空间中实施起来困难或繁琐的有用功能。

在创建识别码、兑换码和超出整数范围的数字字符串等用途中,经常需要生成包含特定字符的随机字符串。
要在用户界面实现此操作,需要在循环中获取输入字符串的随机偏移量,尽管这很简单,但却需要多行代码。
而且,还容易出现微小的错误,如忘记从字符长度中减去1等。

在生成随机浮点数方面非常有用,例如在需要按照一定概率生成布尔值的情况下。
在用户层面执行此操作看似简单,实际上却很繁琐。
常见的做法是通过Randomizer::getInt()获取的值来进行除法计算,但由于四舍五入误差和浮点数偏差等原因,结果往往不太准确。

提案在\ Random \ Randomizer中添加3个方法和一个枚举。

namespace Random;

final class Randomizer {
    // […]
    public function getBytesFromString(string $string, int $length): string {}
    public function nextFloat(): float {}
    public function getFloat(
        float $min,
        float $max,
        IntervalBoundary $boundary = IntervalBoundary::ClosedOpen
    ): float {}
}

enum IntervalBoundary {
    case ClosedOpen;
    case ClosedClosed;
    case OpenClosed;
    case OpenOpen;
}

将字符串转换为字节从给定的字符串中生成随机字符串。

如果第一个参数$string是一个选择字符串,则当存在相同字符时,该字符被选择的概率会增加。如果所有字符只出现一次,那么选择概率是均匀的。

第二个参数$length表示返回值的长度。

例子

$randomizer = new \Random\Randomizer();

// ランダムなドメイン名
var_dump(sprintf(
    "%s.example.com",
    $randomizer->getBytesFromString('abcdefghijklmnopqrstuvwxyz0123456789', 16)
)); // string(28) "xfhnr0z6ok5fdlbz.example.com"

// 多要素認証の認証コード
var_dump(
    implode('-', str_split($randomizer->getBytesFromString('0123456789', 20), 5))
); // string(23) "09898-46592-79230-33336"

// 小数
var_dump(sprintf(
    '0.%s',
    $randomizer->getBytesFromString('0123456789', 30)
)); // string(30) "0.217312509790167227890877670844"

// aが75%、bが25%で出現するランダム文字列
var_dump(
    $randomizer->getBytesFromString('aaab', 16)
); // string(16) "baabaaaaaaababaa"

// DNA
var_dump(
    $randomizer->getBytesFromString('ACGT', 30)
); // string(30) "CGTAGATCGTTCTGATAGAAGCTAACGGTT"

在创建小数时,请注意最后一位可能为0。

获取浮点数()返回介于参数 $min 和 $max 之间的小数。
区间的边界开放与否取决于参数 $boundary。
默认情况下是半开区间 [$min, $max),包括 $min 但不包括 $max。
返回的值在设定的区间内均匀分布。

在均等分布中,每个区间的值所占比例与其他相同宽度的区间的值所占比例相等。
例如,当调用getFloat(0, 1, IntervalBoundary::ClosedOpen)时,返回值小于0.5的概率与大于或等于0.5的概率相同。
类似地,小于0.1的概率为10%,与大于或等于0.9的概率相同。

我們使用的演算法是來自一篇名為「從區間中提取隨機浮點數的γ-section」的論文中公開的γ-section演算法。

第一个参数$min表示最小值。
第二个参数$max表示最大值。

第三个参数$boundary表示返回值中边界的处理方式。
– 默认为\Random\IntervalBoundary::ClosedOpen,表示[$min, $max)。
– ClosedClosed表示[$min, $max]。
– ClosedOpen表示($min, $max]。
– OpenOpen表示($min, $max)。

示例

$randomizer = new \Random\Randomizer();

// 経緯度
// 緯度は90/-90どちらも可
// 経度は180はあるけど-180はない
var_dump(sprintf(
    "Lat: %+.6f Lng: %+.6f",
    $randomizer->getFloat(-90, 90, \Random\IntervalBoundary::ClosedClosed),
    $randomizer->getFloat(-180, 180, \Random\IntervalBoundary::OpenClosed),
)); // string(32) "Lat: -51.742529 Lng: +135.396328"

下一个浮点数()这个方法与->getFloat(0, 1, \Random\IntervalBoundary::ClosedOpen)是相同的。
它可以更快地处理常见的情况,其中希望获取范围为[0, 1)的数据。

例子

$randomizer = new \Random\Randomizer();

// 50%の確率
var_dump(
    $randomizer->nextFloat() < 0.5
); // bool(true)

// 10%の確率
var_dump(
    $randomizer->nextFloat() < 0.1
); // bool(false)

向后不兼容的更改类名\Random\IntervalBoundary将无法使用。

命名空间\Random是在PHP的随机数中使用的,而且在GitHub上也没有同名的类,所以不会有实际影响。

建议的 PHP 版本PHP8.x 可以进行一种本地化的中国语言释义,但只提供一种选项 :PHP8.x

RFC 影响对于SAPI和扩展模块,以及常量和php.ini的更改,没有任何影响。

提出的投票选择getBytesFromString()被以18票赞成、0票反对通过,getFloat()/ nextFloat()被以16票赞成、1票反对的赞成多数通过。
此功能将在PHP8.3中引入。

补丁和测试getBytesFromString()方法的实现
getFloat()/nextFloat()方法的实现

执行实施https://github.com/php/php-src/commit/ac3ecd03af009d433d4b75d570b3b0f0a3fc0ff7 可以被表述为:
这是一个具有唯一标识符ac3ecd03af009d433d4b75d570b3b0f0a3fc0ff7的提交。
https://github.com/php/php-src/commit/f9a1a903805a0c260c97bcc8bf2c14f2dd76ca76 可以被表述为:
这是一个具有唯一标识符f9a1a903805a0c260c97bcc8bf2c14f2dd76ca76的提交。

参考资料最初的提案
RFC的讨论

感想是什么随机字符串有时候真的很麻烦。
常见的例子是str_shuffle,但它在密码学上不安全,所以并不太适合。
random_bytes是安全的,但返回的是二进制数据,如果想限制为[0-9A-Za-z]时使用起来会有些困难。

因此,我们已经添加了一种实用且方便的方法。
今后,要求创建一个安全的密码,可以很容易地实现。

顺便提一下,根据这个RFC,似乎不是Random Extension 5x和Random Extension Improvement的创作者参与了其中,而是那些在邮件列表上对这些RFC提出了宝贵建议的人制作了它们。

bannerAds