欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

编程思想:巧用位运算重构代码

程序员文章站 2022-04-23 21:49:32
开篇 在一门编程语言中,往往会提供大量的运算符。按功能来分的话,有算术运算符、赋值运算符、关系运算符、逻辑运算符、位运算符等。这些对于大家来说都不陌生。但是,本期的主角『位运算』符相对而言是比较少去使用的。因为位运算符主要针对两个二进制数进行位运算。 巧用位运算能极大的精简代码和提高程序效率。所以, ......

开篇

在一门编程语言中,往往会提供大量的运算符。按功能来分的话,有算术运算符、赋值运算符、关系运算符、逻辑运算符、位运算符等。这些对于大家来说都不陌生。但是,本期的主角『位运算』符相对而言是比较少去使用的。因为位运算符主要针对两个二进制数进行位运算。

巧用位运算能极大的精简代码和提高程序效率。所以,在一些优秀的开源代码中,经常能出现位运算。所以,把位运算这种思想迁移到业务代码里,有时候往往能起到柳暗花明般的重构。

位运算

在 java 语言中,定义了诸多的位运算符,如下所示:

运算符 描述
&
|
~
^ 异或
<< 左移
>> 右移

&(与)

十进制 二进制
3 0 0 1 1
5 0 1 0 1
& 后结果:1 0 0 0 1

即:对应位都为 1 时,才为 1,否则全为 0。

|(或)

十进制 二进制
3 0 0 1 1
5 0 1 0 1
| 后结果 :7 0 1 1 1

即:对应位只要有 1 时,即为 1,否则全为 0。

~(非)

十进制 二进制
3 0 0 1 1
~ 后结果:12 1 1 0 0

即:对应位取反。

异或 ^

十进制 二进制
3 0 0 1 1
5 0 1 0 1
^ 后结果:6 0 1 1 0

即:只要对应为不同即为 1。

使用位运算重构项目

当前我们需要设计一个权限模块,可动态的为用户指定某个文件的操作权限。并且,用户对一个文件的操作权限分为:读(r),写(w),执行(x)。

这是一个很简单的需求,为了描述这种关系,我们会在数据库表关系设计时,定义如下的结构:

数据表:user_file_permission

字段 类型 备注
userid int 用户
fileid int 文件
readable bit 是否可读
writable bit 是否可写
executable bit 是否可执行

映射的模型:userfilepermission

public class userfilepermission {
    /**
     * 用户
     */
    private user user;

    /**
     * 文件
     */
    private file file;

    /**
     * 读操作
     */
    private boolean readable;
    /**
     * 写操作
     */
    private boolean writable;
    /**
     * 执行操作
     */
    private boolean executable;
}

这是常见的实现方式。但考虑下,业务需求千变万化,倘若需要再新增一个下载(d) 操作,是不是需要去额外扩展一个字段。所以,对于长期来讲,有值得重构的空间。

故缺点很明显:

  • 难扩展
  • 繁琐,比如判断是否包含读和执行的操作权限,需要这样写if(xx.isreadable() && xx.isexecutable()),但随着权限操作越来越多时,if代码块也越来越大。

位运算重构

了解 linux 的同学一定知道利用 chmod 来控制文件如何被他人调用。比如针对一个文件,可分别给 user、group、other 设置访问的权限。同时权限操作分为:r(读),w(写),x(执行)。很巧,和我们的需求一样。那我们来看下linux 是如何实现权限控制的。

核心是定义一个整数来代表操作权限,即:r=4,w=2,x=1

  • 若要 rwx 权限,则:4+2+1=7;
  • 若要 rw- 权限,则:4+2=6;
  • 若要 r-x 权限,则:4+1=5。

所以使用 chmod 也可以用数字来表示权限,如下即给 user、group、other 三个维度的对象都设置了代表可读、可写、可执行的权限,代号:7。

chmod 777 file

你可能会想,为什么 r=4,w=2,x=1?聪明的你,肯定想到了——二进制。

权限操作 二进制 十进制
r 0100 4
w 0010 2
x 0001 1

所以借由这个思想,我们对代码进行重构,去掉了readablewritableexecutable 这三个字段,而统一由一个 permissoin 字段来表示,如下所示:

public class userfilepermission {

    /**
     * 可执行(x):0001
     */
    public static final int op_executable = 1;

    /**
     * 可写(w):左移一位:0010
     */
    public static final int op_writable = 1 << 1;

    /**
     * 可读(r):左移二位:0100
     */
    public static final int op_readable = 1 << 2;

    /**
     * 用户
     */
    private user user;
    /**
     * 文件
     */
    private file file;

    /**
     * 权限
     */
    private int permission;
}

其中 permission 的可选项如下表格所示:

permission r w x 描述
1(0001) 0 0 1 可执行
2(0010) 0 1 0 可写
4(0100) 1 0 0 可读
3(0011) 0 1 1 可写、可执行
7(0111) 1 1 1 可读、可写、可执行
0(0000) 0 0 0 禁止

同时,操作权限不是一尘不变的,我们往往需要对其新增、删除、查询。通过位运算,可以非常方便实现。

为当前权限新增一个操作:

public void addop(int op) {
    permission |= op;
}

为当前权限删除一个操作:

public void removeop(int op) {
    permission &= ~op;
}

判断当前权限是否包含指定的操作权限:

public boolean containsop(int op) {
    return (permission & op) == op;
}

判断当前权限是否不包含指定的操作权限:

public boolean notcontainsop(int op) {
    return (permission & op) == 0;
}

当然,这样的重构唯一的缺点就是可读性变差。当然,如果团队对位运算达成共识之后,大家都有一定的了解。相反,可读性还是可以的。同时,位运算的计算非常快,也在一定程度上提升了执行效率。

位运算在 netty 中的体现

我们可以在诸多优秀的开源代码看到位运算的身影。比如 jdk 中有非常多的案例。在此,抛砖引玉,谈谈在 netty 的体现。

netty 的内部提供了 skip 的注解,用来表明一个 channelhandler 的某个方法不需要被执行,即跳过。我们来看下netty 是如何实现的。

final class channelhandlermask {

    // using to mask which methods must be called for a channelhandler.
    static final int mask_exception_caught = 1;
    static final int mask_channel_registered = 1 << 1;
    static final int mask_channel_unregistered = 1 << 2;
    static final int mask_channel_active = 1 << 3;
    static final int mask_channel_inactive = 1 << 4;
    static final int mask_channel_read = 1 << 5;
    static final int mask_channel_read_complete = 1 << 6;
    static final int mask_user_event_triggered = 1 << 7;
    static final int mask_channel_writability_changed = 1 << 8;
    static final int mask_bind = 1 << 9;
    static final int mask_connect = 1 << 10;
    static final int mask_disconnect = 1 << 11;
    static final int mask_close = 1 << 12;
    static final int mask_deregister = 1 << 13;
    static final int mask_read = 1 << 14;
    static final int mask_write = 1 << 15;
    static final int mask_flush = 1 << 16;

    private static final int mask_all_inbound = mask_exception_caught | mask_channel_registered |
            mask_channel_unregistered | mask_channel_active | mask_channel_inactive | mask_channel_read |
            mask_channel_read_complete | mask_user_event_triggered | mask_channel_writability_changed;
    private static final int mask_all_outbound = mask_exception_caught | mask_bind | mask_connect | mask_disconnect |
            mask_close | mask_deregister | mask_read | mask_write | mask_flush;

    /**
     * calculate the {@code executionmask}.
     */
    private static int mask0(class<? extends channelhandler> handlertype) {
        int mask = mask_exception_caught;
        try {
            if (channelinboundhandler.class.isassignablefrom(handlertype)) {
                mask |= mask_all_inbound;

                if (isskippable(handlertype, "channelregistered", channelhandlercontext.class)) {
                    mask &= ~mask_channel_registered;
                }
                if (isskippable(handlertype, "channelunregistered", channelhandlercontext.class)) {
                    mask &= ~mask_channel_unregistered;
                }
                if (isskippable(handlertype, "channelactive", channelhandlercontext.class)) {
                    mask &= ~mask_channel_active;
                }
                if (isskippable(handlertype, "channelinactive", channelhandlercontext.class)) {
                    mask &= ~mask_channel_inactive;
                }
                if (isskippable(handlertype, "channelread", channelhandlercontext.class, object.class)) {
                    mask &= ~mask_channel_read;
                }
                if (isskippable(handlertype, "channelreadcomplete", channelhandlercontext.class)) {
                    mask &= ~mask_channel_read_complete;
                }
                if (isskippable(handlertype, "channelwritabilitychanged", channelhandlercontext.class)) {
                    mask &= ~mask_channel_writability_changed;
                }
                if (isskippable(handlertype, "usereventtriggered", channelhandlercontext.class, object.class)) {
                    mask &= ~mask_user_event_triggered;
                }
            }
           ...

            if (isskippable(handlertype, "exceptioncaught", channelhandlercontext.class, throwable.class)) {
                mask &= ~mask_exception_caught;
            }
        } catch (exception e) {
            // should never reach here.
            platformdependent.throwexception(e);
        }

        return mask;
    }
}

上述代码将主干代码剥离出后,其实核心逻辑很简单:

// 添加了所有
mask |= mask_all_inbound;
// 如果该 handler 的 xx 方法标注了 @skip 注解,则将他剔除
if (isskippable(handlertype, "xx", channelhandlercontext.class)) {
    mask &= ~xx;
}

因为 nettypipeline 是个职责链,它需要判断当前的 method是否被允许执行。使用 (ctx.executionmask & mask) == 0 来表示当前是否被禁止调用。如果是的话,则忽略,继续迭代,直到找到允许被调用的 handler。 如下所示:

private abstractchannelhandlercontext findcontextinbound(int mask) {
    abstractchannelhandlercontext ctx = this;
    do {
        ctx = ctx.next;
    } while ((ctx.executionmask & mask) == 0);
    return ctx;
}

小结

本文为大家展示了如何使用二进制以及位运算来重构代码。显而易见,代码量及其精简。同时这种思想也大量出现在开源代码中,值得学习。