2021-06-29


哈工大软件构造阅读心得4-1: 代码评审


本文参考:MIT Reading4哈工大学长汉化

注:这个系列是本人看过阅读资料之后,对看过的内容进行的总结

每一个变量只有一个目的

在 dayOfYear 这个例子中, dayOfMonth被用来做不同意义的值:一开始它是这个月的第几天,最后它是返回的结果(是今年的第几天)。

不要重利用参数,也不要重利用变量。在现在的计算机中,变量不是一个稀缺的资源。当你需要的时候就声明一个(命名一个易理解的名字),不需要它的时候就停止使用。如果你的变量在前面几行代表一个意思,在后面又代表另一个意思,你的读者会很困惑的。

另外,这不仅仅是一个易理解的问题,它也和我们的“远离bug”以及“可改动性”有关。

特别地,方法的参数不应该被修改(这和“易改动性”相关——在未来如果这个方法的某一部分想知道参数传进来的初始值,那么你就不应该在半路修改它)。所以应该使用final关键词修饰参数(这样Java编译器就会对它进行静态检查,防止重引用),然后在方法内部声明其他的变量使用。

2021-06-29

难闻的例子#2

在 dayOfYear
中有一个bug——它没有正确处理闰年。为了修复它,我们写了一个判断闰年的方法:

public static boolean leap(int y) {
    String tmp = String.valueOf(y);
    if (tmp.charAt(2) == '1' || tmp.charAt(2) == '3' || tmp.charAt(2) == 5 || tmp.charAt(2) == '7' || tmp.charAt(2) == '9') {
        if (tmp.charAt(3)=='2'||tmp.charAt(3)=='6') return true; /*R1*/
        else
            return false; /*R2*/
    }else{
        if (tmp.charAt(2) == '0' && tmp.charAt(3) == '0') {
            return false; /*R3*/
        }
        if (tmp.charAt(3)=='0'||tmp.charAt(3)=='4'||tmp.charAt(3)=='8')return true; /*R4*/
    }
    return false; /*R5*/
}

注:charAt:Returns the char value at the specified index. An index ranges from 0 to length() - 1. The first char value of the sequence is at index 0, the next at index 1, and so on, as for array indexing.

阅读

2016

当你判断2016年时会发生什么:

leap(2016)

[x] 在 R1处返回true

[ ] 在 R2处返回false

[ ] 在 R3处返回false

[ ] 在 R4处返回true

[ ] 在 R5处返回false

[ ] 在程序运行前报错

[ ] 在程序运行时报错

2017

当你判断2017年时会发生什么:

leap(2017)

[ ] 在 R1处返回true

[x] 在 R2处返回false

[ ] 在 R3处返回false

[ ] 在 R4处返回true

[ ] 在 R5处返回false

[ ] 在程序运行前报错

[ ] 在程序运行时报错

2050

当你判断2050年时会发生什么:

leap(2050)

[ ] 在 R1处返回true

[x] 在 R2处返回false

[ ] 在 R3处返回false

[ ] 在 R4处返回true

[ ] 在 R5处返回false

[ ] 在程序运行前报错

[ ] 在程序运行时报错

10016

当你判断10016年时会发生什么:

leap(10016)

[ ] 在 R1处返回true

[ ] 在 R2处返回false

[ ] 在 R3处返回false

[ ] 在 R4处返回true

[x] 在 R5处返回false

[ ] 在程序运行前报错

[ ] 在程序运行时报错

916

当你判断916年时会发生什么:

leap(916)

[ ] 在 R1处返回true

[ ] 在 R2处返回false

[ ] 在 R3处返回false

[ ] 在 R4处返回true

[ ] 在 R5处返回false

[ ] 在程序运行前报错

[x] 在程序运行时报错

幻数

在这个方法了幻数一共出现了几次(重复的也按多次算)?

12

DRY

假设你写了一个帮助方法:

public static boolean isDivisibleBy(int number, int factor) { 
	return number % factor == 0; 
}

接着 leap() 使用这个 isDivisibleBy(year, …)方法重写,并且正确的使用 leap year algorithm中描述的算法,这时该方法中会出现几个幻数?

3

使用好的名称

好的方法名和变量名都是比较长而且能自我解释的。这种时候注释通常都不必要,因为名字就已经解释了它的用途。

通常来说, tmp, temp, 和 data
这样变量名是很糟糕的(最懒的程序员的标志)。每一个局部变量都是暂时的(temporary),每一个变量也都是数据(data)。所以这些命名都是无意义的。我们应该使用更长、更有描述性的命名。

每一种语言都有它自己的命名传统。

在Java中:

methodsAreNamedWithCamelCaseLikeThis (方法)

variablesAreAlsoCamelCase(变量,注:驼峰命名法

CONSTANTS_ARE_IN_ALL_CAPS_WITH_UNDERSCORES (常量)

ClassesAreCapitalized (类)

packages.are.lowercase.and.separated.by.dots (包)

ALL_CAPS_WITH_UNDERSCORES 是用来表示 static final这样的常量,所有在方法内部声明的方法,包括用final修饰的,都使用camelCaseNames.

方法的名字通常都是动词,例如 getDate 或者 isUpperCase,而变量和类的名字通常都是名词。尽量选用简洁的命名,但是要避免缩写

练习

更好的方法名

public static boolean leap(int y) {
    String tmp = String.valueOf(y);
    if (tmp.charAt(2) == '1' || tmp.charAt(2) == '3' || tmp.charAt(2) == 5 || tmp.charAt(2) == '7' || tmp.charAt(2) == '9') {
        if (tmp.charAt(3)=='2'||tmp.charAt(3)=='6') return true;
        else
            return false;
    }else{
        if (tmp.charAt(2) == '0' && tmp.charAt(3) == '0') {
            return false;
        }
        if (tmp.charAt(3)=='0'||tmp.charAt(3)=='4'||tmp.charAt(3)=='8')return true;
    }
    return false;
}

下面哪一个方法名比 leap这个名字 更合适?

[ ] leap

[x] isLeapYear

[ ] IsLeapYear

[ ] is_divisible_by_4

下面哪一个变量名比 tmp 更合适?

[ ] leapYearString

[x] yearString

[ ] temp

[ ] secondsPerDay

[ ] s

使用空白符

在代码行中添加一些一致的空格有利于人们的阅读。leap这个例子就将很多代码“杂糅”在一起——记得加一些空格。

另外要注意的是,永远不要使用Tab字符(注:即\t)来进行缩进,只能使用空格字符。这里强调的是不要使用\t字符,不是说键盘上的这个按键(注:很多编辑器和IDE都会自动把Tab按键作为设置好几个连续的空格输入)。因为不同的工具在显示\t字符的时候长度不一样,有的是8个空格,有的是4个空格,有的是2个空格,所以在你用“git diff”或者其他的编辑器看同一份代码很可能就会显示的不一样。永远将你用的文本编辑器设置为按下Tab键输入空格而非\t。

难闻的例子#3

下面是本次阅读的第三个例子,它呈现了我们剩下要讲的要点:

2021-06-29

不要使用全局变量

在Java中,全局变量被声明为 public static 。public修饰符代表它可以从任何地方访问,而 static代表这个变量只会有一个实例化的值。

然而,如果我们加上另一个关键词final : public static final,并且这个变量的类型是不可更改的,那么这个对象就变成了一个“全局常量”。

长期占用内存

全局变量生命周期长,程序运行期一直存在,始终占有那块存储区。

难以定位修改

全局变量是公共的,全部函数都可以访问,难以定位全局变量在哪里被修改,加大了调试的难度。

使函数难以理解

使用全局变量的函数,需要关注全局变量的值,增加了理解的难度,增加了耦合性。

初始化顺序

全局变量的初始化顺序不定,如果全局变量之间有依赖,有可能导致某些变量初始化失败呢,引起莫名其妙bug。

污染命名空间

全局变量会污染命名空间,在函数中局部变量会覆盖全局的值,会出现同一个变量名多个值的情况,造成困惑。

增加耦合性

修改全部变量会影响所有用到它的模块,不利于调试。

在快照图当中的各种变量

在我们画快照图的时候,区别不同种类的变量是很重要的:

  1. 方法里面的局部变量

  2. 一个实例化对象中的实例化变量

  3. 一个类中的静态变量

当方法被调用的时候,局部变量产生,当方法返回时,局部变量消失。如果一个方法被多次同时调用(例如递归),这些方法里面的局部变量互相独立,彼此不会影响。

当一个对象用new实例化后,对象中实例化的变量产生,当这个对象被垃圾回收时,这个变量消失。每一个实例化对象都有它自己的实例化变量。

当程序启动时(更准确点说是包含该静态变量的类被加载的时候),静态变量就产生了,它会一直存活到程序结束。

下面这个例子中使用到了上面三种变量:

2021-06-29

下面这个快照图描述了各个变量之间的区别:

2021-06-29

局部变量p和args显示在一个栈帧中,它们在main函数被调用的时候动态生成,main函数返回时它们也会跟着消失。而println是在main函数调用它的时候生成的。

实例化变量 value 会在每一个Payment类型的对象中出现。

静态变量 taxRate出现在Payment类型的对象之外,因为它是属于Payment这个类的。任何数量的 Payment类型的对象都可以被创建或销毁(同时它们含有的实例化变量 value也会跟着一起创建和销毁),但是在整个程序中有且仅有一个Payment类,所以这里也有且仅有一个Payment.taxRate变量。 System.out是另一个在这段代码中使用到的静态变量,所以在快照图中也将它显示出来了。

练习

辨识全局变量

在上面第三个例子中,哪一些是全局变量?
[ ] countLongWords

[ ] n

[x] LONG_WORD_LENGTH

[x] longestWord

[ ] word

[ ] words

final的效果

使用final关键词是避开使用全局变量风险的一个办法。如果我们在第三个例子中分别对以下变量使用final关键词会发生什么?

n -> 成为常量

LONG_WORD_LENGTH -> 成为常量

longestWord -> 程序运行前报错

word -> 程序运行前报错

words -> 成为常量

方法应该返回结果,而不是打印它

System.out.意味着如果你想在另一个地方使用它,其中结果可能会做其他的用途,例如参与运算而不是显示出来,程序就得重写。

唯一的例外是debug的时候,你需要将一些关键值打印出来。但是这一部分代码不会是你设计的一部分,只有在debug的时候才能出现。

上一篇:LeetCode 最后一个单词的长度


下一篇:【leetcode】字符串-转换成小写字母