PHP中的位掩码用于设置?

15

位和位掩码是我一直以来都很难理解的东西,但我想学习如何在PHP中使用它们进行设置等操作。

最终我找到了一个声称可以完美实现这个功能的类,据我所知,它似乎能够正常工作,但我不确定这是否是最佳做法。我会在下面附上带有示例代码的类文件,以便展示其工作方式。

如果你有相关经验,请告诉我如何改进它,无论是为了性能还是其他方面。我真的很想学习这个知识点,虽然我一直在阅读相关文献,但到目前为止,它对我来说仍然很难理解。

以下是该类...

<?php
    class bitmask
    {
        /**
         * This array is used to represent the users permission in usable format.
         *
         * You can change remove or add valuesto suit your needs.
         * Just ensure that each element defaults to false. Once you have started storing
         * users permsisions a change to the order of this array will cause the
         * permissions to be incorectly interpreted.
         *
         * @type Associtive array
         */
        public $permissions = array(
                                    "read" => false,
                                    "write" => false,
                                    "delete" => false,
                                    "change_permissions" => false,
                                    "admin" => false
                                    );

        /**
         * This function will use an integer bitmask (as created by toBitmask())
         * to populate the class vaiable
         * $this->permissions with the users permissions as boolean values.
         * @param int $bitmask an integer representation of the users permisions.
         * This integer is created by toBitmask();
         *
         * @return an associatve array with the users permissions.
         */
        public function getPermissions($bitMask = 0)
        {
            $i = 0;
            foreach ($this->permissions as $key => $value)
            {
                $this->permissions[$key] = (($bitMask & pow(2, $i)) != 0) ? true : false;

                // Uncomment the next line if you would like to see what is happening.
                //echo $key . " i= ".strval($i)." power=" . strval(pow(2,$i)). "bitwise & = " . strval($bitMask & pow(2,$i))."<br>";
                $i++;
            }
            return $this->permissions;
        }

        /**
         * This function will create and return and integer bitmask based on the permission values set in
         * the class variable $permissions. To use you would want to set the fields in $permissions to true for the permissions you want to grant.
         * Then call toBitmask() and store the integer value.  Later you can pass that integer into getPermissions() to convert it back to an assoicative
         * array.
         *
         * @return int an integer bitmask represeting the users permission set.
         */
        function toBitmask()
        {
            $bitmask = 0;
            $i = 0;
            foreach ($this->permissions as $key => $value)
            {

                if ($value)
                {
                    $bitmask += pow(2, $i);
                }
                $i++;
            }
            return $bitmask;
        }
    }
?>

我如何将权限设置/保存为位掩码值?

<?php
    /**
     * Example usage
     * initiate new bitmask object
     */
    $perms = new bitmask();

    /**
     * How to set permissions for a user
     */
    $perms->permissions["read"] = true;
    $perms->permissions["write"] = true;
    $perms->permissions["delete"] = true;
    $perms->permissions["change_permissions"] = true;
    $perms->permissions["admin"] = false;

    // Converts to bitmask value to store in database or wherever
    $bitmask = $perms->toBitmask();  //in this example it is 15
    $sql = "insert into user_permissions (userid,permission) values(1,$bitmask)";
    echo $sql; //you would then execute code to insert your sql.
?>

根据位掩码值获取数组项并返回true/false的示例代码...

<?php
    /**
     * Example usage to get the bitmask value from database or session/cache.... then put it to use.
     * $permarr returns an array with true/false for each array value based on the bit value
     */
    $permarr = $perms->getPermissions($bitmask);

    if ($permarr["read"])
    {
        echo 'user can read: <font color="green">TRUE</font>';
    }
    else {
        echo 'user can read: <font color="red">FALSE</font>';
    }

    //user can WRITE permission
    if ($permarr["write"])
    {
        echo '<br>user can write: <font color="green">TRUE</font>';
    }
    else {
        echo '<br>user can write: <font color="red">FALSE</font>';
    }
?>

1
我知道这不是你要求的,但考虑到你正在将其保存到数据库中,并将其保存为单个值,我必须问一下,为什么?你已经使该列无法在数据库中进行任何过滤。你担心空间吗? - Marvo
4
我不明白为什么你说这个列是无用的。按照他的方式,他可以轻松地通过执行以下操作检索具有阅读权限的用户:select userid from user_permissions where (permission & 1) = 1 - grandouassou
@florian 不是不能用,但你会喜欢使用掩码位来代替一个 can_read 的列吗?我个人更喜欢使用后者,但也很想知道其他人的想法... - alex
1
当然,can_read 更明确,但如果您有更多的设置,也许您不想要 15 列 can_boocan_zoo - grandouassou
@florian 我可能会显得很蠢,但是一个位掩码只能有8个开/关值吗? - alex
这取决于您的存储方式。如果您的数据库列具有double类型,则最多可以拥有32个开/关值。我从未需要超过32个设置,但我认为如果列类型可以存储2 ^ 64数字,则可能可以增加到64个。 - grandouassou
2个回答

33

位域是处理标志或任何布尔值集合的非常方便和高效的工具。

要理解它们,您首先需要了解二进制数的工作原理。之后,您应该查看有关按位运算符的手册条目,并确保您知道按位AND、OR和左/右移位如何工作。

一个位域不过就是一个整数值。假设我们的位域大小固定为1个字节。计算机使用二进制数,因此如果我们的数字值为 29,则实际上会在内存中找到 0001 1101

使用按位AND(&)和按位OR(|)可以分别读取和设置该数字的每个位。它们都以两个整数作为输入,并对每个位执行AND/OR操作。

要读取数字的第一位,您可以执行以下操作:

  0001 1101 (=29, our number)
& 0000 0001 (=1, bit mask)
= 0000 0001 (=1, result)

正如您所看到的,您需要一个特殊的数字,只有我们感兴趣的位被设置,这就是所谓的“位掩码”。在我们的情况下,它是1。要读取第二位,我们必须将位掩码中的1向左推一个位置。我们可以使用左移运算符($number << 1)或将数字乘以2来实现。

  0001 1101
& 0000 0010
= 0000 0000 (=0, result) 

对于我们的数字中的每个二进制位,您都可以这样做。我们的数字和位掩码的二进制 AND 运算结果为零,这意味着该位未被“设置”,或者为非零整数,这意味着该位已被设置。

如果您想设置其中一个位,可以使用按位 OR:

  0001 1101
| 0010 0000 (=32, bit mask)
= 0011 1101 (=29+32)

不过,当你想要“清除”一位时,你需要采用不同的方式。

一个更普遍的方法是:

// To get bit n
$bit_n = ($number & (1 << $n)) != 0
// Alternative
$bit_n = ($number & (1 << $n)) >> $n

// Set bit n of number to new_bit
$number = ($number & ~(1 << $n)) | ($new_bit << $n)

一开始看起来可能有点晦涩,但实际上很容易理解。

你现在可能已经发现了,位域是一种相当低级的技术。这就是为什么我建议不要在PHP或数据库中使用它们。如果你想要一堆标志,那可能还好,但如果需要其他任何东西,你真的不需要它们。

你发布的类对我来说有点特别。例如,像... ? true : false这样的写法非常不好。如果你想使用位域,最好定义一些常量并使用上面描述的方法。设计一个简单的类并不难。

define('PERM_READ', 0);
define('PERM_WRITE', 1);

class BitField {
    private $value;

    public function __construct($value=0) {
        $this->value = $value;
    }

    public function getValue() {
        return $this->value;
    }

    public function get($n) {
        return ($this->value & (1 << $n)) != 0;
    }

    public function set($n, $new=true) {
        $this->value = ($this->value & ~(1 << $n)) | ($new << $n);
    }
    public function clear($n) {
        $this->set($n, false);
    }
}


$bf = new BitField($user->permissions);

if ($bf->get(PERM_READ)) {
    // can read
}

$bf->set(PERM_WRITE, true);
$user->permissions = $bf->getValue();
$user->save();

我并没有尝试过这个答案中的任何代码,但即使它不能直接运行,它也应该能让你有所启发。

请注意,每个位域最多只能包含32个值。


1
如果你觉得需要进一步改进这个例子,你应该用const替换define(),并且可能实现一些自动化的getter/setter或ArrayAccess以获得额外的优越性。 - svens
@svens 我正在考虑将其更改为更自包含,不确定您所说的ArrayAccess是什么意思。 - JasonDavis
@jasondavis 请参考这篇手册页面: http://php.net/manual/en/class.arrayaccess.php。它使得你的对象行为类似于一个数组,因此你可以使用 $bf[PERM_READ] 这样的代码来获取值。 - svens
@svens 再次感谢您的帮助,我选择了您的答案,但我真的想完善这个类,您给了我一个很好的起点,但我仍然在寻求其他人的帮助,他们可以创造比我更好的最终结果。如果您有兴趣查看我使用您创建的相同类的新问题,请访问https://dev59.com/02435IYBdhLWcg3wfgMf,上面有200点赏金,希望从其他人那里获得更多好的想法和建议,如果您有时间,请随时贡献 +11111 - JasonDavis
@svens感谢您将此发布到公共领域! - Theodore R. Smith
显示剩余5条评论

9
以下是关于如何定义位掩码的方法:

这里是定义位掩码的方法。

// the first mask.  In binary, it's 00000001
define('BITWISE_MASK_1', 1 << 0); // 1 << 0 is the same as 1

// the second mask.  In binary, it's 00000010
define('BITWISE_MASK_2', 1 << 1);

// the third mask.  In binary, it's 00000100
define('BITWISE_MASK_3', 1 << 2);

要检查一个位掩码是否存在(在这种情况下是在函数参数中),请使用按位与运算符。

function computeMasks($masks) {
    $masksPresent = array();
    if ($masks & BITWISE_MASK_1)
        $masksPresent[] = 'BITWISE_MASK_1';
    if ($masks & BITWISE_MASK_2)
        $masksPresent[] = 'BITWISE_MASK_2';
    if ($masks & BITWISE_MASK_3)
        $masksPresent[] = 'BITWISE_MASK_3';
    return implode(' and ', $masksPresent);
}

这个方法能够生效是因为当你对两个字节进行OR操作(比如,0000000100010000),你会得到它们的合并结果:00010001。如果你对结果和原始掩码(比如00000001)进行AND操作,你会得到一个掩码,如果存在的话(在这个例子中就是00000001)。否则,你会得到零。


虽然您提供了资源,但如果您提供一些更详细的信息,这将对通过Google等搜索引擎查找此问题的人更有帮助。或者更简单地说:链接不是答案。 - Brian Driscoll
@Brian:好的,回答已添加。我之前有些犹豫,因为我不确定他是否在问这个问题(现在仍然不确定)。 - Jonah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接