好代码具备的特性
- 正确:代码应当正确处理所有预期输入(expected input)和非法输入(unexpected input)。
- 高效:不管是从空间上还是从时间上来衡量,代码都要尽可能地高效运行。所谓的“高效”不仅是指在极限情况下的渐近效率( asymptotic efficiency,大O记法),同时也包括实际运行的效率。也就是说,在计算O时间时,你可以忽略某个常量因子,但在实际环境中,该常量因子可能有很大影响。
- 简洁:代码能写成10行就不要写成100行。这样开发人员才能尽快写好代码。
- 易读:要确保其他开发人员能读懂你的代码,并弄清楚来ᴜ去ᑡ。易读的代码会有适当注释,实现思路也简单易懂。这就意味着,那些包含诸多位操作的花ά的代码不见得就是“好”代码。
- 易维护:在产品生命周期内,代码经过适当修改就能应对需求的变化。此外,无论对于原开发人员还是其他开发人员,代码都应该易于维护。
编写好代码的一些具体方法
1. 多用数据结构
示例:编写一个函数,对两个简单的多项式求和,其形式为Ax^a+ Bx^b +…(其中系数和指数为任意正实数或负实数),即多项式的每一项都是一个常量乘以某个数的n次幂。
这个函数有多种实现方式。
- 最差的实现方式
最差的实现方式就是将多个多项式存储为一个double型数组,其中第k个元素对应的是多项式中x^k的系数。采用这种结构有一定的问题,如此一来,多项式就不能含有负的或是非整数指数。要想用这种方法来表示x^1000多项式的话,这个数组就得包含1000个元素。
int* sum(double *poly1, double *poly2)
{
...
}
- 较差的实现方式
一种不算最差的实现方式是将多项式存为一对数组coefficients和exponents。采用这种方法,多项式的所有项可以按顺序存放,只要系数和指数配对,多项式的第 i 项表示为coefficients[i] * x^exponents[i]。
采用这种实现方式,如果coefficients[p] = k 和 exponents[p] = m,则第p项为kx^m。尽管这么做没有上面那种解法的限制,但还是很凌乱。一个多项式就要用两个数组记录。如果两个数组长度不同,多项式就会出现“未定义”值。而要返回多项式更是麻烦,因为一下子得返回两个数组。
??? sum(double *coeffs1, double *expon1, double *coeffs2, double *expon2)
{
...
}
- 较好的实现方式
对于这个问题,较好的实现方式就是为多项式设计一种数据结构。
struct polyTerm {
double coefficient;
double exponent;
}
PolyTerm* sum(PolyTerm *poly1, PolyTerm *poly2) {
...
}
可以看到,通过设计一种合理的数据结构,可以较好地解决示例中的问题。
2. 适当重用代码
示例:检查某个二进制数(以字符串形式传入)是否等于以字符串表示的十六进制数。
我们可以通过善用代码重用巧妙解决该问题。
int compareBinToHex(char *binary, char *hex)
{
int n1 = convertToBase(binary, 2);
int n2 = convertToBase(hex, 16);
if(n1<0 || n2<0)
return FALSE;
if(n1 == n2)
return TRUE;
else
return FALSE;
}
//将指定进制字符串转换成整型值
int convertToBase(char *number, int base)
{
if(base<2 || (base>10 && base!=16))
return -1;
int value = 0;
int length = strlen(number);
for(int i=length-1; i>=0; i--){
int digit = digitToValue(number[i]);
if(digit < 0 || digit >= base)
return -1;
int exp = length-i-1; //指数值
value += digit * pow(base, exp);
}
return value;
}
//将数字字符准换成对应的整数数值
int digitToValue(char c)
{
if(c>='0' && c<='9')
return c-'0';
else if(c>='a' && c<='f')
return c-'a'+10;
else if(c>='A' && c<='F')
return c-'A'+10;
else
return -1;
}
我们本可以实现两套代码,分别实现二进制数和十六进制数的转换,但这么做只会加大代码的编写难度,而且维护起来也更难。相反,我们还是通过编写convertToBase() 和 digitToValue() 的方法来重用代码。
3. 模块化
编写模块化代码是指将孤立的代码块划分为相应的方法(函数)。这有助于让代码更易读,可读性和可测试性更强。
示例:编写交换整型数组中的最大和最小元素的代码。
实现方法1:将全部代码写在一个函数里。
void swapMinMax(int *array, int len)
{
int minIndex = 0;
for(int i=1; i<len; i++){
if(array[i] < array[minIndex])
minIndex = i;
}
int maxIndex = 0;
for(int i=1; i<len; i++){
if(array[i] > array[maxIndex])
maxIndex = i;
}
int temp = array[minIndex];
array[minIndex] = array[maxIndex];
array[maxIndex] = temp;
}
实现方法2:采用更模块化的方式,将相对孤立的代码块隔离到对应的函数中。
int getMinIndex(int *array, int len)
{
int minIndex = 0;
for(int i=1; i<len; i++){
if(array[i] < array[minIndex])
minIndex = i;
}
return minIndex;
}
int getMaxIndex(int *array, int len)
{
int maxIndex = 0;
for(int i=1; i<len; i++){
if(array[i] > array[maxIndex])
maxIndex = i;
}
return maxIndex;
}
void swap(int *array, int m, int n)
{
int temp = array[m];
array[m] = array[n];
array[n] = temp;
}
void swapMinMaxBetter(int *array, int len)
{
int minIndex = getMinIndex(array, len);
int maxIndex = getMaxIndex(array, len);
swap(array, minIndex, maxIndex);
}
虽然前面的非模块化代码看起来也不怎么糟,但模块化代码的一大好处在于它易于测试,因为每一部分都可以单独验证。随着代码越来越复杂,编写模块化代码就变得越发重要。模块化的代码也更易阅读和维护。
4. 灵活、健壮
编写灵活、通用的代码,也就意味着使用变量,而不是在代码里直接把值写死,或者使用模板/泛型来解决问题。要是有办法编写代码解决更普遍的问题,那我们就应该这么做。
5. 错误检查
写代码很细心的人有一个明显的特征,那就是他不会想当然地处理输入信息。相反,他会用ASSERT 语句或if语句仔细验证输入数据是否合理。
比如,回到前面那段将基数为i的进制数(比如基数为2或16)转换成整数的代码。
//将指定进制字符串转换成整型值
int convertToBase(char *number, int base)
{
if(base<2 || (base>10 && base!=16))
return -1;
int value = 0;
int length = strlen(number);
for(int i=length-1; i>=0; i--){
int digit = digitToValue(number[i]);
if(digit < 0 || digit >= base)
return -1;
int exp = length-i-1; //指数值
value += digit * pow(base, exp);
}
return value;
}
在第4行,我们检查基数是否有效(假定除16外,大于10的基数都是无效的,没有标准的字符串表示形式)。在第10行,我们另加了一处错误检查:确保每个数字都落在允许范围内。
诸如此类的错误检查在实际的产品代码中至关重要,在编写代码时绝不能掉以轻心。