IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    编写高质量代码

    ABOER发表于 2016-08-02 02:10:03
    love 0

    我们知道,int和double能表示的数值的范围不同。其中,64位有符号整数的范围是[-9223372036854775808,9223372036854775807],而64位无符号整数的范围是[0,18446744073709551615]。这两个区间有一定的overlap,而double可以表示的范围更大。

    现在,需要编写两个函数:给定一个double型的value,判断这个value是否是一个合法的int64_t或者uint64_t。本文说的“合法”,是指数值上落在了范围内。

    bool is_valid_uint64(const Double &value);
    
    bool is_valid_int64(const Double &value);

    这里我们用Double而不是double,原因是我们的double不是基础数据类型,而是通过一定方法实现的ADT,这个ADT的成员函数有:

    class Double
    {
      public:
        int get_next_digit(bool &is_decimal);
        bool is_zero();
        bool is_neg();
    };

    通过调用get_next_digit,可以返回一个数字,不断调用它,可以得到所有digits。举个例子,对于值为45.67的一个Double对象,调用它的get_next_digit成员函数将依次得到

    4 is_decimal = false //表示整数部分

    5 is_decimal = false //表示整数部分

    6 is_decimal = true //表示小数部分

    7 is_decimal = true //表示小数部分

    当get_next_digit返回-1时,表示读取完毕。

    如何利用Double类里的成员函数,来实现is_valid_uint64和is_valid_int64这两个函数呢?

    一些新手可能会写这样的代码:

    bool is_valid_uint64(const Double &value)
    {
      bool is_valid = true;
      int digits[2000];
      int counts = 0;
      if (value.is_zero()) {
        is_valid = true;
      } else if(value.is_neg()) {
        is_valid = false;
      } else {
        bool is_decimal = false;
        int digit = 0;
        while((digit=value.get_next_digit(is_decimal)) != -1) {
          if (is_decimal) {
            is_valid = false;
            break;
          } else {
            digits[counts++] = digit;
          }
        }
        uint64_t tmp = 0;
        uint64_t base = 1;
        for (int i = counts - 1; i >= 0; i++) {
          tmp += digits[i] * base;
          if (tmp > UINT64_MAX) {
            is_valid = false;
            break;
          }
          base *= 10;
        }
      }
      return is_valid;
    }
    bool is_valid_int64(const Double &value)
    {
      bool is_valid = true;
      int digits[2000];
      int counts = 0;
      if (value.is_zero()) {
        is_valid = true;
      } else if(value.is_neg()) {
        bool is_decimal = false;
        int digit = 0;
        while((digit=value.get_next_digit(is_decimal)) != -1) {
          if (is_decimal) {
            is_valid = false;
            break;
          } else {
            digits[counts++] = digit;
          }
        }
        uint64_t tmp = 0;
        uint64_t base = 1;
        for (int i = counts - 1; i >= 0; i++) {
          tmp += digits[i] * base;
          tmp *= -1;
          if (tmp  INT64_MIN) {
            is_valid = false;
            break;
          }
          base *= 10;
        }
      } else {
        bool is_decimal = false;
        int digit = 0;
        while((digit=value.get_next_digit(is_decimal)) != -1) {
          if (is_decimal) {
            is_valid = false;
            break;
          } else {
            digits[counts++] = digit;
          }
        }
        uint64_t tmp = 0;
        uint64_t base = 1;
        for (int i = counts - 1; i >= 0; i++) {
          tmp += digits[i] * base;
          if (tmp > INT64_MAX) {
            is_valid = false;
            break;
          }
          base *= 10;
        }
      }
      return is_valid;
    }

    这样的代码,存在诸多问题。

    设计问题

    不难发现,两个函数存在很多相似甚至相同的代码;而同一个函数内部,也有不少代码重复。重复的东西往往不是好的。重构?

    性能问题

    先获得所有digits,然后从最低位开始向最高位构造值,效率较低。难道没有可以从最高位开始,边获得边计算,不需要临时数组存储所有digits的方法吗?

    正确性问题

    随便举几个例子:

    第24行,tmp += digits[i] * base;有没有考虑到可能的溢出呢?

    第68行,难道有小数部分就一定不是合法的int64吗?那么,123.000?嗯?

    规范问题

    帅哥,这么多代码,一行注释都没有,这样真的好吗?

    因此,毫无疑问,这是烂代码,不合格的代码,需要重写的代码。

    以下是我个人认为比较好的设计和实现,仅供参考。

    bool is_valid_uint64(const Double &value)
    
    {
    
      bool ret = false;
    
      check_range(value, &ret, NULL);
    
      return ret;
    
    }
    
    
    
    bool is_valid_int64(const Double &value)
    
    {
    
      bool ret = false;
    
      check_range(value, NULL, &ret);
    
      return ret;
    
    }
    
    
    
    void check_range(const Double &value,
    
                     bool *is_valid_uint64,
    
                     bool *is_valid_int64) const
    
    {
    
      /*
    
       * 对于一个负数的value,它不可能是一个合法的uint64.
    
       * 因此,只剩下三种可能:
    
       * I 输入的value是负数,判断是否是合法的int64
    
       * II 输入的value是正数,判断是否是合法的uint64
    
       * III 输入的value是正数,判断是否是合法的int64
    
       * 对于第II、III这两种情况:只要判断value的值是否超过uint64、int64的上界即可
    
       * 对于第I种情况,我们利用-A > -B 等价于 A 
    
       * 因此,在第I种情况里,可以判断value的绝对值,是否超过int64的最小值的绝对值即可。
    
       * (int64的最小值的绝对值?那不就是int64的最大值?哦,不!)
    
       * 因此,不管哪种情况,判断绝对值是否超过某个上界即可。
    
       * 这三种情况,上界不一样。把三个上界存到了一个二维数组THRESHOLD里
    
      */
    
    
    
      bool *is_valid = NULL;
    
      static const int FLAG_INT64 = 0;
    
      static const int FLAG_UINT64 = 1;
    
      static const int SIGN_NEG = 0;
    
      static const int SIGN_POS = 1;
    
      int flag = FLAG_INT64;
    
      if (NULL != is_valid_uint64) {
    
        is_valid = is_valid_uint64;
    
        flag = FLAG_UINT64;
    
      } else {
    
        is_valid = is_valid_int64;
    
      }
    
      *is_valid = true;
    
      if (value.is_zero()) {
    
        //do nothing。0是合法的uint64,也是合法的int64
    
      } else {
    
        int sign = value.is_neg() ? SIGN_NEG : SIGN_POS;
    
        if ((SIGN_NEG == sign) && (FLAG_UINT64 == flag)) {
    
          *is_valid = false;//负数不可能是合法的uint64
    
        } else {
    
          uint64_t valueUint = 0;
    
          static uint64_t ABS_INT64_MIN = 9223372036854775808ULL;
    
                                             //int64        uint64
    
          static uint64_t THRESHOLD[2][2] = { {ABS_INT64_MIN, 0}, //neg
    
                                             {INT64_MAX,     UINT64_MAX} }; //pos
    
          int digit = 0;
    
          bool is_decimal = false;
    
          while ((digit = value.get_next_digit(is_decimal)) != -1) {
    
            if (!is_decimal) {
    
              //为了防止溢出,我们不能这么写:
    
              //"value * 10 + digit > THRESHOLD[index]"
    
              if (valueUint > (THRESHOLD[sign][flag] - digit) / 10) {
    
                *is_valid = false;
    
                break;
    
              } else {
    
                valueUint = valueUint * 10 + digit;//霍纳法则(也叫秦九韶算法)
    
              }
    
            } else {
    
              if (!digit) {
    
                *is_valid = false; //小数部分必须是0;否则不可能是合法的uint64、int64
    
                break;
    
              }
    
            }
    
          }
    
        }
    
      }
    
    }

    代码规范

    团队的代码规范,一般由领导和大佬们制定后,大家统一实行。这里面有几个问题:

    真的需要代码规范吗?

    言下之意,制定和执行代码规范是否浪费时间?

    答案是:It depends。如果项目很庞大、代码质量要求很高,那么,制定和执行代码规范所花费的时间,将大大少于后期因为不规范开发带来的种种调试和维护成本。如果是小打小闹的代码,就无所谓了。

    代码规范的制定为什么这么难?

    原因众多,其中一个很重要的部分是团队每个人的口味和观点不尽相同。就代码风格而言,有人喜欢对内置类型变量i使用i++,有人坚持认为应该使用++i不管i是不是复杂类型。因此,制定代码规范需要在讨论之后最后拍板决定,这里面甚至需要独裁!是的,独裁!

    代码规范制定需要注意什么事项?

    如果代码规范限制太松,那么等于没有规范;如果太严,大大影响开发效率。这里面的尺度,需要根据项目需要、团队成员特点全面考量,进行取舍。

    需要注意的是,没有任何一种代码规范是完美的。例如,在C++中,如果启用异常,那么代码的流程将会被各种异常处理中断,各种try catch throw让代码很不美观;如果禁用异常,也就是在开发的过程中不能使用异常特性,那么团队成员可能因为长期没有接触这项语言feature而造成知识和技能短板。

    代码风格举例

    举两个我认为比较重要、比较新鲜、比较有趣的代码风格。

    1,使用引用需要判空吗?

    void f(int &p);
    void g(int *p);

    我们都知道,在g中,使用*p前需要对p是否为NULL进行判断,那么f呢?如果质量非常关键、代码安全非常重要的场景,那么实际上,也是需要的。因为调用者可能这样:

    int *q = NULL;
    //......
    f(*q);

    因此,需要在f里增加if(NULL == &p)的判断。

    2,级联if else语句。

    首先看一个我个人认为不好的代码风格:

    int f(int a, int b)
    {
      if (a >= 1) {
        if (b >= 1) {
          if (a >= b) {
            //do sth
          } else {
            //error1
          }
        } else {
          //error2
        }
      } else {
        //error3
      }
    }

    这个函数的核心在于do sth部分。其实我们可以改写为级联if-else形式,如下:

    int f(int a, int b)
    {
      if (a < 1) {
        //error3
      } else if (b < 1) {
        //error2
      } else if (a < b) {
        //error1
      } else {
        //so, a>=1 && b>=1 && a>=b
        //do sth
      }
    }

    是不是优美多了?前面只做一些错误处理、前期准备、参数检查等,最后的else分支做实实在在的功能性事情。

    Code Review

    什么是Code Review?

    很多人把它翻译为代码审查,我觉得太政治味了。程序员尤其是新手写完代码后,可能会有风格问题(比如不符合团队的代码规范)、安全性问题(比如忘记指针判空)、优雅性问题(比如大量冗余代码)、正确性问题(比如算法设计错误),那么在发布代码到公共库之前,提交给师兄或者mentor,让他帮你review一下代码,并提出可能的问题和建议,让你好好修改。这样的过程,就叫做Code Review。

    我的天呐,那这不是很占用时间?

    是的。一个写代码,一个看代码,看代码的时间可能并不比全新写一份代码少。那么,这又是何必呢?

    主要的原因有:

    1,review确实占用了开发时间,然而开发,或者说写代码,其实只占很少的时间比例。很多时间花在debug、调试、写文档、需求分析、设计算法、维护等等上。

    2,代码质量非常重要,这点时间投入是值得的。与其后期苦逼追bug,不如前期多投入点时间和人力。

    3,培养新人,让新手更快成长。

    如何更好的执行Code Review

    这里给几点建议:

    1,不走过场。走过场,还不如不要这个流程。

    2,作为Reviewer,看不懂代码就把作者拉过来,当面询问,不要不懂装懂,也不要爱面子不好意思问。

    3,作为Coder,心里要有感激之情。真的。不要得了便宜还卖乖,感恩reviewer,感激reviewer对自己的进步和成长所做出的贡献,所花费的心血。中国人里狼心狗肺、忘恩负义、不懂感恩的人还算少吗?

    4,作为Coder,给Reviewer Review之前,请先做单元测试并确保通过,并自己尝试先整体看一遍自己本次提交的代码。注意,不要给别人提还没调试通过的代码,这是非常不尊重别人的表现。

    质量保证

    1,测试不是专属QA的活儿,对自己写的代码提供质量保证,是程序员的职责。QA要负责的,是系统的质量,不是模块的质量。

    2,测试,需要意识,需要坚持。我发现C++程序员、前端程序员的测试意识或者说质量意识最强;数据科学家或者数据工程师的质量意识最差,很多人甚至不写测试用例。当然,这不怪他们,毕竟,有时候代码里有个bug,准确率和召回率会更高。

    3,测试用例的编写和设计需要保证一定的代码覆盖率,力求让每个分支和流程的代码都走到,然后分析运行结果是否是符合期望的,不要只考虑正确路径上的那些分支。

    4,测试用例的编写和设计力求全面,考虑到方方面面。以非常经典的二分搜索为例:

    int binary_search(int *p, int n, int target, int &idx);

    binary_search函数返回值为0表示成功执行,输出参数idx返回target在有序数组p中(第一次出现)的位置,-1表示不存在。

    那么测试用例至少应该涵盖:

    • p为NULL的情况
    • 数组大小n分别为负数、0、1、2时情况
    • 数组p不是有序数组的情况
    • target在数组中出现0次、1次、n次的情况

    你是否都考虑到了呢?

    4,有时候,自己书写测试用例显得刀耕火种,现在已经有很多辅助的工具,读者可以自行google一下。

    相关文章

    • 如何在 Linux 下检测内存泄漏
    • C++虚表,你搞懂了吗?
    • 趣文:C++ 程序员离职之前要做的事
    • Notepad++源码编译及其分析
    • C++11 中的 Defaulted 和 Deleted 函数
    • 你可能不知道的陷阱:C#委托和事件的困惑
    • 标准模板库(STL)使用入门(上)
    • 漫谈 C++:良好的编程习惯与编程要点
    • 在C++中实现Python的切片
    • C++之父:C++ 的五个普遍误解(3)

    编写高质量代码,首发于文章 - 伯乐在线。



沪ICP备19023445号-2号
友情链接