位掩码的正确使用方法是什么?

8

嘿嘿,我有一个关于位掩码的问题。我现在知道它们是什么以及它们可以在哪里使用。 我想要为特定的组存储特定权限,例如BUILD、BREAK和INTERACT,也可能是更多。下面的代码应该可以做到这一点,但我不确定这是否是正确的“风格”。

这个想法是在这里使用前3位来存储第一组的权限,然后使用下一个三位来存储第二组,以此类推。 所以我的问题是,这是一个好方法还是有更好的方法?

public class Test {
    private int permissions = 0;

    /**
     * The amount of permissions, currently: {@link #BREAK}, {@link #BUILD}, {@link #INTERACT}
     */
    private static final int PERMISSIONS = 3;
    /**
     * The different permissions
     */
    public static final int BUILD = 1, BREAK = 2, INTERACT = 4;
    /**
     * The different groups
     */
    public static final int ALLIANCE = 0, OUTSIDERS = 1;

    public void setPermissions(int permissions, int group)
    {
        this.permissions = permissions << group * PERMISSIONS;
    }

    public void addPermissions(int permission, int group)
    {
        setPermissions(this.permissions | permission, group);
    }

    public boolean hasPermission(int permission, int group)
    {
        return (permissions & permission << group * PERMISSIONS) == permission;
    }
}

编辑:我希望尽可能少地使用内存,因为我需要存储大量数据。

编辑:我还需要将其存储在SQL数据库中,但不应该出现问题。

2个回答

13

你肯定早晚会看到这样的回答,所以这里就是:

尽管位掩码的使用可以说是所有备选方案中最快速且占用内存最少的,但正确使用也非常容易出错,除了在一些极端情况下,大多数情况下都不建议使用。它是一个经典的低级工具。如果使用正确,效果惊人;如果滥用,则可能会造成严重后果。

因此,正确的方法是使用更高级别的抽象,即enumsEnumSets。速度和内存消耗是可比的,虽然稍微差一些。在一般情况下,它们绰绰有余。根据您的确切上下文和需求,有许多实现方法。其中之一可能是这样的:

public enum Permission {
    BUILD, BREAK, INTERACT;
}

public class Permissions {
    private final Set<Permission> alliance = EnumSet.noneOf(Permission.class);
    private final Set<Permission> outsiders = EnumSet.noneOf(Permission.class);

    public Set<Permission> alliance() {
        return alliance;
    }

    public Set<Permission> outsiders() {
        return outsiders;
    }
}

仅仅这个变化就使得您能够做到与之前完全相同的事情,但是有两个不同之处:

  1. 现在它是类型安全的和更加防错,我认为不需要重新发明轮子。
  2. 它使用更多的内存。虽然不多,因为这么小的枚举集通常只是一个long


编辑以回答OP在评论中提出的将EnumSet存储到数据库的问题:

是的,这可能是一个问题,因为存储一个int要容易得多。 如果您仍然考虑使用EnumSet,则我可以想到以下几种可能性:

  1. 查看StackOverflow。 之前有人试图解决这个问题。

  2. 保存EnumSet中值的名称:

Permissions p = new Permissions();
p.alliance().addAll(EnumSet.of(Permission.BREAK, Permission.BUILD));
for (Permission permission : p.alliance()) {
    System.out.println(permission);
}

您接下来可以轻松地重建这些值:

for (String word : stringsFromDtb) {
    p.alliance.add(Permission.valueOf(word));
}
  • 保存序数。这是非常危险的,因为您可以轻松地通过更改Permission枚举来破坏它。此外,任何随机数字都可能被输入以破坏它。

  • Permissions p = new Permissions();
    p.alliance().addAll(EnumSet.of(Permission.BREAK, Permission.BUILD));
    for (Permission permission : p.alliance()) {
        System.out.println(permission.ordinal());
    }
    

    您随后可以轻松重建这些值:

    for (int ordinal : ordinalsFromDtb) {
        p.alliance.add(Permission.values()[ordinal]);
    }
    
  • EnumSet按照通常的方式进行序列化,然后直接存储二进制数据或者BASE64编码的数据。嗯。

  • ---

    EDIT之后的修改:

    针对您有关为您的enum值创建索引的注释,以便在将来更改或重新排序时仍然可以正常工作。使用enums有一种简单的方法来做到这一点!它基本上是位字段和enums之间的中间方法,它保留了类型安全性和所有enum功能,并且仍具有位字段的优势。

    public enum Permission {
        /* I like to have binary literals in place of bit fields,
         * but any notation will work */
        BUILD   (0b0001),
        BREAK   (0b0010),
        INTERACT(0b0100);
    
        private final int index;
    
        private Permission(int index) {
            this.index = index;
        }
    
        public int index() {
            return index;
        }
    }
    

    然后您将把这些索引保存到数据库中,并且只需确保从中解析的内容是正确的。此外,在将来,仅将不需要的枚举值注释掉(而不是删除)可以帮助您保留其可见性,以便您不会占用其索引。或者只需将其标记为@Deprecated,您就不必删除任何内容;)


    我也考虑过这种方式,所以又有一个问题,因为我需要将权限存储在 SQL 数据库中。所以我会给每个枚举类型分配一个 ID,并将这些 ID 存储在数据库中的字符串中。是否有更好的方法? - maxammann
    @p000ison,将权限存储在参考表中,并使用连接表来分配权限。 - Gus
    @Slanec 所以,我不能以二进制格式存储它,因为可能会有网站访问这些数据,同时存储名称也可能不是最好的选择,因为可能会更改枚举的名称。为了避免风险,我将为枚举条目定义ID并使用它们。但我不确定是否真的要使用EnumSet,或者我可以使用位掩码。我看到位掩码的唯一问题是,如果扩展权限系统,它将会破坏 :/ - maxammann
    也许我会使用long类型,并为进一步的权限预留足够的空间。 - maxammann
    @p000ison 我知道我来晚了,但是我找到了这个类可以将EnumSets存储和加载到/从long中,而且易于操作且不易出错 - Petr Janeček
    显示剩余3条评论

    3

    我认为你有几个地方出现了错误

    public void setPermissions(int permissions, int group)
    {
        this.permissions = permissions << group * PERMISSIONS;
    }
    

    当设置 group 的权限时,这会清除其他组的所有权限。如果您想保留其他组的权限,

    // clear permissions for `group`
    this.permissions &= ~(0x7 << group * PERMISSIONS);
    // set permissions for `group`
    this.permissions |= permissions << group * PERMISSIONS;
    

    这将不影响其他组的权限。
    public void addPermissions(int permission, int group)
    {
        setPermissions(this.permissions | permission, group);
    }
    

    这将您想要为添加的权限与所有组的权限混合。我认为您想要的是:

    this.permissions |= permission << group * PERMISSIONS;
    

    只需为添加权限而无需更改其他任何内容。

    public boolean hasPermission(int permission, int group)
    {
        return (permissions & permission << group * PERMISSIONS) == permission;
    }
    

    您正在错误地进行转换,如果 group > 0,那么按位与将不会在最低三位中有任何位。

    return ((permissions >> group *PERMISSIONS) & permission) == permission;
    

    2
    +1!这也是为什么我们不再在生产中使用复杂的位掩码:)如果正确使用,它可以创造奇迹,但有时复杂性太大了。 - Petr Janeček
    @Daniel Fischer 哦,谢谢,这是我第一次真正使用它,谢谢 :P - maxammann

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