代码可读性.md 9.8 KB
Newer Older
C
CyC2018 已提交
1
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
C
CyC2018 已提交
2
<!-- GFM-TOC -->
C
CyC2018 已提交
3 4 5 6
* [一、可读性的重要性](#一可读性的重要性)
* [二、用名字表达代码含义](#二用名字表达代码含义)
* [三、名字不能带来歧义](#三名字不能带来歧义)
* [四、良好的代码风格](#四良好的代码风格)
C
CyC2018 已提交
7
* [五、为何编写注释](#五为何编写注释)
C
CyC2018 已提交
8 9 10 11 12 13 14 15 16 17 18 19 20
* [六、如何编写注释](#六如何编写注释)
* [七、提高控制流的可读性](#七提高控制流的可读性)
* [八、拆分长表达式](#八拆分长表达式)
* [九、变量与可读性](#九变量与可读性)
* [十、抽取函数](#十抽取函数)
* [十一、一次只做一件事](#十一一次只做一件事)
* [十二、用自然语言表述代码](#十二用自然语言表述代码)
* [十三、减少代码量](#十三减少代码量)
* [参考资料](#参考资料)
<!-- GFM-TOC -->


# 一、可读性的重要性
C
CyC2018 已提交
21 22 23 24 25 26 27

编程有很大一部分时间是在阅读代码,不仅要阅读自己的代码,而且要阅读别人的代码。因此,可读性良好的代码能够大大提高编程效率。

可读性良好的代码往往会让代码架构更好,因为程序员更愿意去修改这部分代码,而且也更容易修改。

只有在核心领域为了效率才可以放弃可读性,否则可读性是第一位。

C
CyC2018 已提交
28
# 二、用名字表达代码含义
C
CyC2018 已提交
29 30 31

一些比较有表达力的单词:

C
CyC2018 已提交
32 33 34 35 36 37
|  单词 |  可替代单词 |
| :---: | --- |
|  send | deliver、dispatch、announce、distribute、route  |
| find  |  search、extract、locate、recover |
| start| launch、create、begin、open|
| make | create、set up、build、generate、compose、add、new |
C
CyC2018 已提交
38

C
CyC2018 已提交
39
使用 i、j、k 作为循环迭代器的名字过于简单,user_i、member_i 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高。
C
CyC2018 已提交
40 41 42

为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。

C
CyC2018 已提交
43
# 三、名字不能带来歧义
C
CyC2018 已提交
44 45 46

起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。

C
CyC2018 已提交
47
布尔相关的命名加上 is、can、should、has 等前缀。
C
CyC2018 已提交
48

C
CyC2018 已提交
49 50 51
- 用 min、max 表示数量范围;
- 用 first、last 表示访问空间的包含范围;
- begin、end 表示访问空间的排除范围,即 end 不包含尾部。
C
CyC2018 已提交
52

C
fix  
CyC2018 已提交
53
<div align="center"> <img src="pics/05907ab4-42c5-4b5e-9388-6617f6c97bea.jpg"/> </div><br>
C
CyC2018 已提交
54

C
CyC2018 已提交
55
# 四、良好的代码风格
C
CyC2018 已提交
56 57 58 59 60 61

适当的空行和缩进。

排列整齐的注释:

```java
C
CyC2018 已提交
62 63 64
int a = 1;   // 注释
int b = 11;  // 注释
int c = 111; // 注释
C
CyC2018 已提交
65 66
```

C
CyC2018 已提交
67
语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致。
C
CyC2018 已提交
68

C
CyC2018 已提交
69
# 五、为何编写注释
C
CyC2018 已提交
70

C
CyC2018 已提交
71
阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。
C
CyC2018 已提交
72 73 74 75 76 77 78

不能因为有注释就随便起个名字,而是争取起个好名字而不写注释。

可以用注释来记录采用当前解决办法的思考过程,从而让读者更容易理解代码。

注释用来提醒一些特殊情况。

C
CyC2018 已提交
79
用 TODO 等做标记:
C
CyC2018 已提交
80

C
CyC2018 已提交
81
| 标记 | 用法 |
C
CyC2018 已提交
82
|---|---|
C
CyC2018 已提交
83 84
|TODO| 待做 |
|FIXME| 待修复 |
X
Xing Liu 已提交
85
|HACK| 粗糙的解决方案 |
C
CyC2018 已提交
86
|XXX| 危险!这里有重要的问题 |
C
CyC2018 已提交
87

C
CyC2018 已提交
88
# 六、如何编写注释
C
CyC2018 已提交
89 90 91 92

尽量简洁明了:

```java
C
CyC2018 已提交
93 94 95
// The first String is student's name
// The Second Integer is student's score
Map<String, Integer> scoreMap = new HashMap<>();
C
CyC2018 已提交
96 97 98
```

```java
C
CyC2018 已提交
99 100
// Student's name -> Student's score
Map<String, Integer> scoreMap = new HashMap<>();
C
CyC2018 已提交
101 102 103 104 105
```

添加测试用例来说明:

```java
C
CyC2018 已提交
106 107 108 109
// ...
// Example: add(1, 2), return 3
int add(int x, int y) {
    return x + y;
C
CyC2018 已提交
110 111 112 113 114
}
```

使用专业名词来缩短概念上的解释,比如用设计模式名来说明代码。

C
CyC2018 已提交
115
# 七、提高控制流的可读性
C
CyC2018 已提交
116 117 118 119

条件表达式中,左侧是变量,右侧是常数。比如下面第一个语句正确:

```java
C
CyC2018 已提交
120 121
if (len < 10)
if (10 > len)
C
CyC2018 已提交
122 123
```

C
CyC2018 已提交
124
只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑,否则应该拆分成 if / else;
C
CyC2018 已提交
125

C
CyC2018 已提交
126
do / while 的条件放在后面,不够简单明了,并且会有一些迷惑的地方,最好使用 while 来代替。
C
CyC2018 已提交
127

C
CyC2018 已提交
128
如果只有一个 goto 目标,那么 goto 尚且还能接受,但是过于复杂的 goto 会让代码可读性特别差,应该避免使用 goto。
C
CyC2018 已提交
129

C
CyC2018 已提交
130
在嵌套的循环中,用一些 return 语句往往能减少嵌套的层数。
C
CyC2018 已提交
131

C
CyC2018 已提交
132
# 八、拆分长表达式
C
CyC2018 已提交
133 134 135 136

长表达式的可读性很差,可以引入一些解释变量从而拆分表达式:

```python
C
CyC2018 已提交
137 138
if line.split(':')[0].strip() == "root":
    ...
C
CyC2018 已提交
139 140
```
```python
C
CyC2018 已提交
141 142 143
username = line.split(':')[0].strip()
if username == "root":
    ...
C
CyC2018 已提交
144 145 146 147 148
```

使用摩根定理简化一些逻辑表达式:

```java
C
CyC2018 已提交
149 150
if (!a && !b) {
    ...
C
CyC2018 已提交
151 152 153
}
```
```java
C
CyC2018 已提交
154 155
if (!(a || b)) {
    ...
C
CyC2018 已提交
156 157 158
}
```

C
CyC2018 已提交
159
# 九、变量与可读性
C
CyC2018 已提交
160

C
CyC2018 已提交
161
**去除控制流变量** 。在循环中通过使用 break 或者 return 可以减少控制流变量的使用。
C
CyC2018 已提交
162 163

```java
C
CyC2018 已提交
164 165 166 167 168 169 170
boolean done = false;
while (/* condition */ && !done) {
    ...
    if ( ... ) {
        done = true;
        continue;
    }
C
CyC2018 已提交
171 172
}
```
C
CyC2018 已提交
173 174

```java
C
CyC2018 已提交
175 176 177 178 179
while(/* condition */) {
    ...
    if ( ... ) {
        break;
    }
C
CyC2018 已提交
180 181 182
}
```

C
CyC2018 已提交
183
**减小变量作用域** 。作用域越小,越容易定位到变量所有使用的地方。
C
CyC2018 已提交
184

C
CyC2018 已提交
185
JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函数变量,submitted 变量控制函数不会被提交两次。第一个实现中 submitted 是全局变量,第二个实现把 submitted 放到匿名函数中,从而限制了起作用域范围。
C
CyC2018 已提交
186 187

```js
C
CyC2018 已提交
188 189 190 191 192 193
submitted = false;
var submit_form = function(form_name) {
    if (submitted) {
        return;
    }
    submitted = true;
C
CyC2018 已提交
194 195 196 197
};
```

```js
C
CyC2018 已提交
198 199 200 201 202 203 204 205 206
var submit_form = (function() {
    var submitted = false;
    return function(form_name) {
        if(submitted) {
            return;
        }
        submitted = true;
    }
}());  // () 使得外层匿名函数立即执行
C
CyC2018 已提交
207 208
```

C
CyC2018 已提交
209
JavaScript 中没有用 var 声明的变量都是全局变量,而全局变量很容易造成迷惑,因此应当总是用 var 来声明变量。
C
CyC2018 已提交
210 211 212

变量定义的位置应当离它使用的位置最近。

C
CyC2018 已提交
213
**实例解析** 
C
CyC2018 已提交
214 215 216 217

在一个网页中有以下文本输入字段:

```html
C
CyC2018 已提交
218 219 220 221
<input type = "text" id = "input1" value = "a">
<input type = "text" id = "input2" value = "b">
<input type = "text" id = "input3" value = "">
<input type = "text" id = "input4" value = "d">
C
CyC2018 已提交
222 223
```

C
CyC2018 已提交
224
现在要接受一个字符串并把它放到第一个空的 input 字段中,初始实现如下:
C
CyC2018 已提交
225 226

```js
C
CyC2018 已提交
227 228 229 230 231 232 233 234 235 236 237 238 239 240
var setFirstEmptyInput = function(new_alue) {
    var found = false;
    var i = 1;
    var elem = document.getElementById('input' + i);
    while (elem != null) {
        if (elem.value === '') {
            found = true;
            break;
        }
        i++;
        elem = document.getElementById('input' + i);
    }
    if (found) elem.value = new_value;
    return elem;
C
CyC2018 已提交
241 242 243 244 245
}
```

以上实现有以下问题:

C
CyC2018 已提交
246 247 248
- found 可以去除;
- elem 作用域过大;
- 可以用 for 循环代替 while 循环;
C
CyC2018 已提交
249 250

```js
C
CyC2018 已提交
251 252 253 254 255 256 257 258 259 260 261
var setFirstEmptyInput = function(new_value) {
    for (var i = 1; true; i++) {
        var elem = document.getElementById('input' + i);
        if (elem === null) {
            return null;
        }
        if (elem.value === '') {
            elem.value = new_value;
            return elem;
        }
    }
C
CyC2018 已提交
262 263 264
};
```

C
CyC2018 已提交
265
# 十、抽取函数
C
CyC2018 已提交
266 267 268 269 270 271 272 273

工程学就是把大问题拆分成小问题再把这些问题的解决方案放回一起。

首先应该明确一个函数的高层次目标,然后对于不是直接为了这个目标工作的代码,抽取出来放到独立的函数中。

介绍性的代码:

```java
C
CyC2018 已提交
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
int findClostElement(int[] arr) {
    int clostIdx;
    int clostDist = Interger.MAX_VALUE;
    for (int i = 0; i < arr.length; i++) {
        int x = ...;
        int y = ...;
        int z = ...;
        int value = x * y * z;
        int dist = Math.sqrt(Math.pow(value, 2), Math.pow(arr[i], 2));
        if (dist < clostDist) {
            clostIdx = i;
            clostDist = value;
        }
    }
    return clostIdx;
C
CyC2018 已提交
289 290 291 292 293 294
}
```

以上代码中循环部分主要计算距离,这部分不属于代码高层次目标,高层次目标是寻找最小距离的值,因此可以把这部分代替提取到独立的函数中。这样做也带来一个额外的好处有:可以单独进行测试、可以快速找到程序错误并修改。

```java
C
CyC2018 已提交
295 296 297 298 299 300 301 302 303 304 305
public int findClostElement(int[] arr) {
    int clostIdx;
    int clostDist = Interger.MAX_VALUE;
    for (int i = 0; i < arr.length; i++) {
        int dist = computDist(arr, i);
        if (dist < clostDist) {
            clostIdx = i;
            clostDist = value;
        }
    }
    return clostIdx;
C
CyC2018 已提交
306 307 308 309 310 311 312
}
```

并不是函数抽取的越多越好,如果抽取过多,在阅读代码的时候可能需要不断跳来跳去。只有在当前函数不需要去了解某一块代码细节而能够表达其内容时,把这块代码抽取成子函数才是好的。

函数抽取也用于减小代码的冗余。

C
CyC2018 已提交
313
# 十一、一次只做一件事
C
CyC2018 已提交
314 315 316 317 318

只做一件事的代码很容易让人知道其要做的事;

基本流程:列出代码所做的所有任务;把每个任务拆分到不同的函数,或者不同的段落。

C
CyC2018 已提交
319
# 十二、用自然语言表述代码
C
CyC2018 已提交
320 321 322

先用自然语言书写代码逻辑,也就是伪代码,然后再写代码,这样代码逻辑会更清晰。

C
CyC2018 已提交
323
# 十三、减少代码量
C
CyC2018 已提交
324 325 326 327 328

不要过度设计,编码过程会有很多变化,过度设计的内容到最后往往是无用的。

多用标准库实现。

C
CyC2018 已提交
329
# 参考资料
C
CyC2018 已提交
330

C
CyC2018 已提交
331
- Dustin, Boswell, Trevor, 等. 编写可读代码的艺术 [M]. 机械工业出版社, 2012.