程序设计实践读书笔记(一)
Chapter One: 风格
正如第一章中所言:“风格的作用主要就是使代码容易读”,所以在第一章讲风格是非常合适的,它不需要多少的专业编程知识,更像是一种好的习惯,使编写出来的代码更容易读,不仅方便他人理解并使用,同时为以后的调试维护提供了便利。
最初写C的时候,变量名也是想到什么字母用什么字母(除了for循环里普遍用i,j,k外),导致的结果就是最后自己都不知道变量代表的是什么(还不喜欢写注释。。。),后来写Java之后开始重视代码的风格,包括命名方式、排版(大部分IDE都有自动格式化功能)、注释格式等等(受Android和JavaWeb框架影响较深)。良好的编程风格的优点无需赘述,这不应该成为编码时去考虑的东西,而应该成为一种潜意识中的习惯。
1.1 名字
完整的程序中必然充斥着各种功能的变量、常量、函数(方法)等,如何通过命名方式来让它们更直观的表达出各自的作用至关重要。
书中主要提到以下几点:
- 全局变量、类和结构应该有说明性文字
- 局部变量名字尽可能短
- 遵守普遍接受的命名惯例,比如指针使用p结尾(nodep),全局变量大写开头(Global)等等
- 不要太纠结于个人编码规矩,比如使用numPending还是num_pending(Java里偏向第一种)
- 函数的命名,如一些判断条件是否满足并返回布尔类型的函数通常都为is开头(如isUpperCase()),JavaBean里的getter & setter,toString()等等
总结一句话,就是命名与作用相关!
Java平台建立了一整套很好的命名惯例(naming convention),包括字面的(typographical)和语法的(grammatical):
字面惯例的例子:
标示符类型 | 例子 |
---|---|
包 | com.xiaoyu.util, org.xiaoyu.dao.impl |
类或者接口 | HttpServlet, AsyncTask(单词首字母大写) |
方法或者域 | toString, equal, isUpperCase(首单词首字母小写,之后单词首字母大写) |
常量域 | IP_ADDR(全部大写,单词之间加下划线) |
局部变量 | stuNumber,mString(与方法命名类似) |
类型参数 | T,E,V,K等等 |
语法惯例的例子:
- JavaBean中强制的getter和setter
- 转换对象类型的方法(toType),如toArray,toString
- 静态工厂常用名:newInstance、getInstance等等
- ………………..
1.2 语句和表达式
现在的大部分IDE和编辑器都有自动格式化代码的功能,因此书中说的那种糟糕的缩进风格会比较少(不过同学们貌似都不熟悉相关IDE快捷键。。。)
关于三元运算符,个人觉得用于选择两个数中较大或者较小的数时使用三元运算符会显得较为简洁(max = a > b? a : b;),其他情况则会造成这句代码过长。
地下活动:
++自增、–自减运算符常用于循环中的计数器,一句代码中不要使用多个++类运算符,尽可能防止可能存在的地下活动。
str[i++] = str[i++] = ' '; //i的更新时刻是不可控制的
I/O也是一种附带地下活动的操作:
scanf("%d %d", &yr, &profit[yr]);
//scanf的所有参数都在函数被真正调用前已经求好值了,所以&profit[yr]实际使用的总是yr原来的值
1.3 一致性和习惯用法
本节主要讲的内容是关于if—else、for的格式、缩排和加括号风格,总结一下就是以下几点:
- if-else体内只有一句语句时可以不加花括号(当然还得考虑上下文)
- 标准的for循环格式 for(i = 0; i < n; i++)
- 避免if-else中“不可能发生”的情况
- 避免使用一系列嵌套的if-else,不利于程序的理解(简洁同样意味着出错率低!)
练习1-8 确定下面的Java程序段中的错误,并把它改写为一个符合习惯的循环。
int count = 0;
while(count < total){
count++;
if(this.getName(count) == nametable.userName()){
return (true);
}
}
//My Answer
for(int count = 0; count < total; count++){
if(this.getName(count) == nametable.userName()){
return (true);
}
}
1.4 函数宏
“有了新型的机器和编译程序,函数宏的缺点就远远超过它能带来的好处。”
函数宏最常见的一个严重问题是:如果一个参数在定义中出现多次,它就可能被多次求值。如果调用时的实际参数带有副作用,结果就会产生一个难以捉摸的错误。下面的代码段来自某个<ctype.h>
,其意图是实现一种字符测试:
#define isupper(c) ((c) >= 'A' && (c) <= 'Z')
参数c在宏的体里出现了两次。如果isupper在下面的上下文中调用:
while(isupper(c = getchar()))
...
那么,每当遇到一个大于等于A的字符,程序就会将它丢掉,而下一个字符将被读入并去与Z做比较。C语言标准是仔细写出的,它允许将isupper及类似函数定义为宏,但要求保证它们的参数只求值一次。因此,上面的实现是错误的。
宏有时还会带来效率问题,宏将在它每次被调用的地方进行实例化,结果会导致被编译的程序变大。
宏是通过文本替换方式实现的!这意味着如果有这么一个宏:
#define square(x) (x) * (x)
那么原本正确的 1 / square(x)(这里的square为函数)将变成 1 / (x) * (x),出现十分隐蔽的错误。因此正确的方式如下:
#define square(x) ((x) * (x)) //加上括号
练习1-9 确定下面的宏定义中的问题:
#define ISDIGIT(c) ((c >= '0') && (c <= '9'))? 1: 0
//和上面的例子错误一样,参数c求值不止一次,导致了进来一个参数和0比较后就被丢弃,第二个参数进来和9比较。
1.5 神秘的数
本章所讲内容总结起来就是一句话:不要让过多的数字“直接“出现在程序中。
- 一个具有原本形式的数对其本身的重要性或作用没提供任何指示性信息,它们会导致程序难以理解和修改:
System.out.println("V = " + 3.1415 * 2 * 5 * 5 * 6); //很难直接了解一堆数字各自表达的意义
C中传统方法是使用#define来进行处理
#define PI 3.1415
#define HEIGHT 6
然而这种预处理方法是有风险的(见1.4),更多时候,整数常熟可以用枚举语句声明:
enum{
MINROW = 1, /* top edge */
MAXROW = 24, /* bottom edge (<=) */
HEIGHT = MAXROW - 4 /* height of bars */
};
C++中可以使用const声明常数,Java中使用final来声明:
const int MAXROW = 24, MAXCOL = 80; //C++
static final int MAXROW = 24, MAXCOL = 80; //Java
- 使用字符形式的常量,不要用整数。如下:
//Normal
if (c >= 65 && c <=90)
......
//Better
if(c >= 'A' && c <= 'Z' )
P.S.
书中提到Java的length域,给出数组的元素个数,常用于循环:
char buf[] = new char[1024];
for(int i = 0;i < buf.length; i++)
联想到《构建之法》上讲程序效能优化中的一段,也是大部分人所习惯的循环方式,代码如下:
for(int i = 0;i < list.size(); i++)
.......
然而上面的代码中,每次for循环都要运行一次list.size(),导致代码的效率降低,正确的方式应该是:
int listSize = list.size();
for(int i = 0;i <listSize; i++)
......
练习1.10 如何重写下面定义,使出错的可能性降到最小?
#define FT2METER 0.3048
#define METER2FT 3.28084
#define MI2FT 5280.0
#define MI2KM 1.609344
#define SQMI2SQKM 2.589988
//My answer:使用enum枚举
enum{
FT2METER = 0.3048;
METER2FT = 3.28084;
.........
};
1.6 注释
书中对于注释的作用已经概述的很精练了:注释应该提供那些不能一下子从代码中看到的东西,或者把那些散布在许多代码里的信息收集到一起。当某些难以捉摸的事情出现时,注释可以帮助澄清情况。因此,诸如如下的注释就没有任何意义了:
zerocount++; /* Increment zero entry counter */
/* return SUCCESS */
return SUCCESS;
注释还可以起到生成帮助文档的作用,如JavaDoc:
* <p>This interface is a member of the
* <a href="{@docRoot}/../technotes/guides/collections/index.html">
* Java Collections Framework</a>.
*
* @param <E> the type of elements in this list
*
* @author Josh Bloch
* @author Neal Gafter
* @see Collection
* @see Set
* @see ArrayList
* @see LinkedList
* @see Vector
* @see Arrays#asList(Object[])
* @see Collections#nCopies(int, Object)
* @see Collections#EMPTY_LIST
* @see AbstractList
* @see AbstractSequentialList
* @since 1.2
*/
public interface List<E> extends Collection<E> {
注释的作用不言而喻,然而真正需要我们去做的是写出更好更易理解的代码,好到不需要那么多注释,好的代码需要的注释远远少于差的代码。
1.7 为何如此费心
第一章的总结部分,用作者给出的”关键的结论“来概括:
好风格应该成为一种习惯。如果你在开始写代码时就关心风格问题,如果你花时间去审视和改进它,你将会逐渐养成一种好的编程习惯。一旦这种习惯变成自动的东西,你的潜意识就会帮你照料许多细节问题,甚至你在工作压力下写出的代码也会更好。