我们知道,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分支做实实在在的功能性事情。
很多人把它翻译为代码审查,我觉得太政治味了。程序员尤其是新手写完代码后,可能会有风格问题(比如不符合团队的代码规范)、安全性问题(比如忘记指针判空)、优雅性问题(比如大量冗余代码)、正确性问题(比如算法设计错误),那么在发布代码到公共库之前,提交给师兄或者mentor,让他帮你review一下代码,并提出可能的问题和建议,让你好好修改。这样的过程,就叫做Code Review。
是的。一个写代码,一个看代码,看代码的时间可能并不比全新写一份代码少。那么,这又是何必呢?
主要的原因有:
1,review确实占用了开发时间,然而开发,或者说写代码,其实只占很少的时间比例。很多时间花在debug、调试、写文档、需求分析、设计算法、维护等等上。
2,代码质量非常重要,这点时间投入是值得的。与其后期苦逼追bug,不如前期多投入点时间和人力。
3,培养新人,让新手更快成长。
这里给几点建议:
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表示不存在。
那么测试用例至少应该涵盖:
你是否都考虑到了呢?
4,有时候,自己书写测试用例显得刀耕火种,现在已经有很多辅助的工具,读者可以自行google一下。