怎样才算好代码?

深藏阁楼爱情的钟 2022-10-08 05:49 357阅读 0赞

好代码具备的特性

  • 正确:代码应当正确处理所有预期输入(expected input)和非法输入(unexpected input)。
  • 高效:不管是从空间上还是从时间上来衡量,代码都要尽可能地高效运行。所谓的“高效”不仅是指在极限情况下的渐近效率( asymptotic efficiency,大O记法),同时也包括实际运行的效率。也就是说,在计算O时间时,你可以忽略某个常量因子,但在实际环境中,该常量因子可能有很大影响。
  • 简洁:代码能写成10行就不要写成100行。这样开发人员才能尽快写好代码。
  • 易读:要确保其他开发人员能读懂你的代码,并弄清楚来ᴜ去ᑡ。易读的代码会有适当注释,实现思路也简单易懂。这就意味着,那些包含诸多位操作的花ά的代码不见得就是“好”代码。
  • 易维护:在产品生命周期内,代码经过适当修改就能应对需求的变化。此外,无论对于原开发人员还是其他开发人员,代码都应该易于维护。

编写好代码的一些具体方法

1. 多用数据结构

示例:编写一个函数,对两个简单的多项式求和,其形式为Ax^a+ Bx^b +…(其中系数和指数为任意正实数或负实数),即多项式的每一项都是一个常量乘以某个数的n次幂。

这个函数有多种实现方式。

  • 最差的实现方式

最差的实现方式就是将多个多项式存储为一个double型数组,其中第k个元素对应的是多项式中x^k的系数。采用这种结构有一定的问题,如此一来,多项式就不能含有负的或是非整数指数。要想用这种方法来表示x^1000多项式的话,这个数组就得包含1000个元素。

  1. int* sum(double *poly1, double *poly2)
  2. {
  3. ...
  4. }
  • 较差的实现方式

一种不算最差的实现方式是将多项式存为一对数组coefficients和exponents。采用这种方法,多项式的所有项可以按顺序存放,只要系数和指数配对,多项式的第 i 项表示为coefficients[i] * x^exponents[i]。

采用这种实现方式,如果coefficients[p] = k 和 exponents[p] = m,则第p项为kx^m。尽管这么做没有上面那种解法的限制,但还是很凌乱。一个多项式就要用两个数组记录。如果两个数组长度不同,多项式就会出现“未定义”值。而要返回多项式更是麻烦,因为一下子得返回两个数组。

  1. ??? sum(double *coeffs1, double *expon1, double *coeffs2, double *expon2)
  2. {
  3. ...
  4. }
  • 较好的实现方式

对于这个问题,较好的实现方式就是为多项式设计一种数据结构。

  1. struct polyTerm {
  2. double coefficient;
  3. double exponent;
  4. }
  5. PolyTerm* sum(PolyTerm *poly1, PolyTerm *poly2) {
  6. ...
  7. }

可以看到,通过设计一种合理的数据结构,可以较好地解决示例中的问题。

2. 适当重用代码

示例:检查某个二进制数(以字符串形式传入)是否等于以字符串表示的十六进制数。

我们可以通过善用代码重用巧妙解决该问题。

  1. int compareBinToHex(char *binary, char *hex)
  2. {
  3. int n1 = convertToBase(binary, 2);
  4. int n2 = convertToBase(hex, 16);
  5. if(n1<0 || n2<0)
  6. return FALSE;
  7. if(n1 == n2)
  8. return TRUE;
  9. else
  10. return FALSE;
  11. }
  12. //将指定进制字符串转换成整型值
  13. int convertToBase(char *number, int base)
  14. {
  15. if(base<2 || (base>10 && base!=16))
  16. return -1;
  17. int value = 0;
  18. int length = strlen(number);
  19. for(int i=length-1; i>=0; i--){
  20. int digit = digitToValue(number[i]);
  21. if(digit < 0 || digit >= base)
  22. return -1;
  23. int exp = length-i-1; //指数值
  24. value += digit * pow(base, exp);
  25. }
  26. return value;
  27. }
  28. //将数字字符准换成对应的整数数值
  29. int digitToValue(char c)
  30. {
  31. if(c>='0' && c<='9')
  32. return c-'0';
  33. else if(c>='a' && c<='f')
  34. return c-'a'+10;
  35. else if(c>='A' && c<='F')
  36. return c-'A'+10;
  37. else
  38. return -1;
  39. }

我们本可以实现两套代码,分别实现二进制数和十六进制数的转换,但这么做只会加大代码的编写难度,而且维护起来也更难。相反,我们还是通过编写convertToBase() 和 digitToValue() 的方法来重用代码。

3. 模块化

编写模块化代码是指将孤立的代码块划分为相应的方法(函数)。这有助于让代码更易读,可读性和可测试性更强。

示例:编写交换整型数组中的最大和最小元素的代码。

实现方法1:将全部代码写在一个函数里。

  1. void swapMinMax(int *array, int len)
  2. {
  3. int minIndex = 0;
  4. for(int i=1; i<len; i++){
  5. if(array[i] < array[minIndex])
  6. minIndex = i;
  7. }
  8. int maxIndex = 0;
  9. for(int i=1; i<len; i++){
  10. if(array[i] > array[maxIndex])
  11. maxIndex = i;
  12. }
  13. int temp = array[minIndex];
  14. array[minIndex] = array[maxIndex];
  15. array[maxIndex] = temp;
  16. }

实现方法2:采用更模块化的方式,将相对孤立的代码块隔离到对应的函数中。

  1. int getMinIndex(int *array, int len)
  2. {
  3. int minIndex = 0;
  4. for(int i=1; i<len; i++){
  5. if(array[i] < array[minIndex])
  6. minIndex = i;
  7. }
  8. return minIndex;
  9. }
  10. int getMaxIndex(int *array, int len)
  11. {
  12. int maxIndex = 0;
  13. for(int i=1; i<len; i++){
  14. if(array[i] > array[maxIndex])
  15. maxIndex = i;
  16. }
  17. return maxIndex;
  18. }
  19. void swap(int *array, int m, int n)
  20. {
  21. int temp = array[m];
  22. array[m] = array[n];
  23. array[n] = temp;
  24. }
  25. void swapMinMaxBetter(int *array, int len)
  26. {
  27. int minIndex = getMinIndex(array, len);
  28. int maxIndex = getMaxIndex(array, len);
  29. swap(array, minIndex, maxIndex);
  30. }

虽然前面的非模块化代码看起来也不怎么糟,但模块化代码的一大好处在于它易于测试,因为每一部分都可以单独验证。随着代码越来越复杂,编写模块化代码就变得越发重要。模块化的代码也更易阅读和维护。

4. 灵活、健壮

编写灵活、通用的代码,也就意味着使用变量,而不是在代码里直接把值写死,或者使用模板/泛型来解决问题。要是有办法编写代码解决更普遍的问题,那我们就应该这么做。

5. 错误检查

写代码很细心的人有一个明显的特征,那就是他不会想当然地处理输入信息。相反,他会用ASSERT 语句或if语句仔细验证输入数据是否合理。

比如,回到前面那段将基数为i的进制数(比如基数为2或16)转换成整数的代码。

  1. //将指定进制字符串转换成整型值
  2. int convertToBase(char *number, int base)
  3. {
  4. if(base<2 || (base>10 && base!=16))
  5. return -1;
  6. int value = 0;
  7. int length = strlen(number);
  8. for(int i=length-1; i>=0; i--){
  9. int digit = digitToValue(number[i]);
  10. if(digit < 0 || digit >= base)
  11. return -1;
  12. int exp = length-i-1; //指数值
  13. value += digit * pow(base, exp);
  14. }
  15. return value;
  16. }

在第4行,我们检查基数是否有效(假定除16外,大于10的基数都是无效的,没有标准的字符串表示形式)。在第10行,我们另加了一处错误检查:确保每个数字都落在允许范围内。

诸如此类的错误检查在实际的产品代码中至关重要,在编写代码时绝不能掉以轻心。

发表评论

表情:
评论列表 (有 0 条评论,357人围观)

还没有评论,来说两句吧...

相关阅读

    相关 怎样的排序算法稳定?

    相信很多程序猿伙伴都会有意或无意用到算法,特别是排序算法,而排序算法则会涉及稳定性,那怎样的排序算法才算稳定呢? 根据百度百科给出对排序算法稳定性的简单描述,假定在待排序的记