程式碼的可讀性比較
以下就程式碼的可讀性即其產生之 binary 進行分析。依其順序,逐漸改進其可讀性。透過比較,我們能較透澈的瞭解可讀性的性質。 下面的程式,是一個分析 HTTP request line 的 parser 。 Request line 的格式如下
GET /path/to/resource HTTP/1.1
程式的目的是將 request line 裡的三個欄位,分別取出為 method 、 uri 和 protocol 。
程式一
最平舖直訴的方式,這大概是每個 programmer 都會經過的階段吧!!
001 #include <stdio.h>
002 #include <string.h>
003 #include <stdlib.h>
004
005 typedef struct {
006 char *method;
007 char *uri;
008 char *proto;
009 } http_req;
010
011 int http_req_parse(http_req *req, const char *buf, int sz) {
012 int i, prev;
013
014 for(i = 0; i < sz; i++) {
015 if(buf[i] == ' ') break;
016 if(buf[i] == '\n' || buf[i] == '\r')
017 return -1;
018 }
019 if(i == sz || i == 0) return -1;
020 req->method = (char *)malloc(i + 1);
021 strncpy(req->method, buf, i);
022 req->method[i] = 0;
023
024 prev = ++i;
025 for(; i < sz; i++) {
026 if(buf[i] == ' ') break;
027 if(buf[i] == '\n' || buf[i] == '\r') break;
028 }
029 if(i == sz || i == prev || buf[i] != ' ') {
030 free(req->method);
031 return -1;
032 }
033 req->uri = (char *)malloc(i - prev + 1);
034 strncpy(req->uri, buf + prev, i - prev);
035 req->uri[i - prev] = 0;
036
037 prev = ++i;
038 for(; i < sz; i++) {
039 if(buf[i] == ' ') break;
040 if(buf[i] == '\n' || buf[i] == '\r') break;
041 }
042 if(i != sz || i == prev) {
043 free(req->method);
044 free(req->uri);
045 return -1;
046 }
047 req->proto = (char *)malloc(i - prev + 1);
048 strncpy(req->proto, buf + prev, i - prev);
049 req->proto[i - prev] = 0;
050
051 return 0;
052 }
053
054 int main(int argc, const char *argv[]) {
055 const char *data = "GET /test.html HTTP/1.1";
056 http_req req;
057
058 if(http_req_parse(&req, data, strlen(data)) < 0) {
059 fprintf(stderr, "error to parse request line!\n");
060 return 1;
061 }
062
063 printf("request line: %s\n", data);
064 printf("method: %s\n", req.method);
065 printf("uri: %s\n", req.uri);
066 printf("protocol: %s\n", req.proto);
067
068 return 0;
069 }
重複的動作
將不斷重複的動作取出,並定個有意義的名稱。如此程式不但變小,而且因為 function 名稱所帶來的意涵,加強了程式的可讀性。由此可知,將程式的部分流程變成 function ,並附於適當的名稱,能改善程式的可讀性。
strncspn() 其實就是 strcspn() 的變形,可看 Linux 或 FreeBSD 的 man page 。而 strndup() 就是 strdup() 的變形。透過這些有意義的名稱,程式碼的流程更有意義,提供更多線索,更適合大腦解讀。
001 #include <stdio.h>
002 #include <string.h>
003 #include <stdlib.h>
004
005 typedef struct {
006 char *method;
007 char *uri;
008 char *proto;
009 } http_req;
010
011 int strncspn(const char *s, int max, const char *charset) {
012 int i, j, cs_sz;
013 char c;
014
015 cs_sz = strlen(charset);
016 for(i = 0; i < max && s[i] != 0; i++) {
017 c = s[i];
018 for(j = 0; j < cs_sz; j++) {
019 if(c == charset[j]) return i;
020 }
021 }
022 return max;
023 }
024
025 char *strndup(const char *s, int max) {
026 int sz = strlen(s);
027 char *buf;
028
029 if(sz > max) sz = max;
030 buf = (char *)malloc(sz + 1);
031 memcpy(buf, s, sz);
032 buf[sz] = 0;
033
034 return buf;
035 }
036
037 int http_req_parse(http_req *req, const char *buf, int sz) {
038 const char *substr, *last, *next;
039 int substr_sz;
040
041 last = buf + sz;
042
043 substr_sz = strncspn(buf, sz, " \r\n");
044 if(substr_sz == sz || substr_sz == 0 || buf[substr_sz] != ' ')
045 return -1;
046 req->method = strndup(buf, substr_sz);
047
048 substr = buf + substr_sz + 1;
049 substr_sz = strncspn(substr, last - substr, " \r\n");
050 next = substr + substr_sz;
051 if(substr_sz == 0 || next == last || *next != ' ') {
052 free(req->method);
053 return -1;
054 }
055 req->uri = strndup(substr, substr_sz);
056
057 substr = next + 1;
058 substr_sz = strncspn(substr, last - substr, " \r\n");
059 next = substr + substr_sz;
060 if(next != last) {
061 free(req->method);
062 free(req->uri);
063 return -1;
064 }
065 req->proto = strndup(substr, substr_sz);
066
067 return 0;
068 }
069
070 int main(int argc, const char *argv[]) {
071 const char *data = "GET /test.html HTTP/1.1";
072 http_req req;
073
074 if(http_req_parse(&req, data, strlen(data)) < 0) {
075 fprintf(stderr, "error to parse request line!\n");
076 return 1;
077 }
078
079 printf("request line: %s\n", data);
080 printf("method: %s\n", req.method);
081 printf("uri: %s\n", req.uri);
082 printf("protocol: %s\n", req.proto);
083
084 return 0;
085 }
邏輯拆離
前面的程式,將 parse 的過程中的數個邏輯交叉混合在一起。下面的程式將這些邏輯個別分離,變成獨立的區塊。將相關邏輯集中處理,而非交錯在一起,導至讀者必需不斷的在邏輯之間切換。
另一方面,將邏輯拆離,能減少透過 variable 保留和傳遞資訊的狀況。如第一和第二個程式,透過變數 i 和 substr 保留和傳遞目前處理的狀態,以在 function 前後傳遞資訊。這迫使讀者必需追蹤 variable 的內容,才能理解每一段程式碼的作用和正確性。
而邏輯拆離後,條件判斷也減少了。使用條件式,經常是一種邏輯上的修補行為,對意外狀況的處置。然而大部分情況並非不可避免的,只需適當的安排,將邏輯拆離,既可避免這種修補的動作。
而邏輯拆離後,每一個程式碼區塊的功能也單純化。讀者更易理解,程式撰寫時,也更能確定程式的正確性。
下例,一開始先把確定換行符號是否在字串裡,以排除換行的狀況。接著取得空白符號的位置。最後複製字串,並成 method 、 uri 和 proto 的內容。
001 #include <stdio.h>
002 #include <string.h>
003 #include <stdlib.h>
004
005 typedef struct {
006 char *method;
007 char *uri;
008 char *proto;
009 } http_req;
010
011 int strncspn(const char *s, int max, const char *charset) {
012 int i, j, cs_sz;
013 char c;
014
015 cs_sz = strlen(charset);
016 for(i = 0; i < max && s[i] != 0; i++) {
017 c = s[i];
018 for(j = 0; j < cs_sz; j++) {
019 if(c == charset[j]) return i;
020 }
021 }
022 return max;
023 }
024
025 char *strndup(const char *s, int max) {
026 int sz = strlen(s);
027 char *buf;
028
029 if(sz > max) sz = max;
030 buf = (char *)malloc(sz + 1);
031 memcpy(buf, s, sz);
032 buf[sz] = 0;
033
034 return buf;
035 }
036
037 int http_req_parse(http_req *req, const char *buf, int sz) {
038 const char *substr, *last;
039 int i;
040 const char *fss[4];
041
042 sz = strncspn(buf, sz, "\r\n");
043 last = buf + sz;
044
045 substr = buf;
046 for(i = 1; i < 4; i++) {
047 fss[i] = substr + strncspn(substr, last - substr, " ");
048 if(fss[i] == last) break;
049 substr = fss[i] + 1;
050 }
051 if(i != 3)
052 return -1;
053
054 fss[0] = buf;
055 fss[3] = last;
056 for(i = 0; i < 3; i++) {
057 if(i > 0) fss[i]++;
058 if((fss[i + 1] - fss[i]) < 1)
059 return -1;
060 }
061
062 req->method = strndup(fss[0], fss[1] - fss[0]);
063 req->uri = strndup(fss[1], fss[2] - fss[1]);
064 req->proto = strndup(fss[2], fss[3] - fss[2]);
065
066 return 0;
067 }
068
069 int main(int argc, const char *argv[]) {
070 const char *data = "GET /test.html HTTP/1.1";
071 http_req req;
072
073 if(http_req_parse(&req, data, strlen(data)) < 0) {
074 fprintf(stderr, "error to parse request line!\n");
075 return 1;
076 }
077
078 printf("request line: %s\n", data);
079 printf("method: %s\n", req.method);
080 printf("uri: %s\n", req.uri);
081 printf("protocol: %s\n", req.proto);
082
083 return 0;
084 }
再次簡化
前一個程式將邏輯分離,這個程式將分離後的程式再改進。例如將重用性高的部分再取出,如 strnchrs() 其實就是 strchr 的變形,這樣的 function 功能單純,可重用性高。再加上賦於一個有意義的名稱,增加了程式的可讀性。
另外,把幾個 loop 替換成直接的 statement ,直接的陳述往往比 loop 和 condition 來的易讀。但,如果重複的次數太多時,當然使用 loop 才是合理的狀況。
001 #include <stdio.h>
002 #include <string.h>
003 #include <stdlib.h>
004
005 typedef struct {
006 char *method;
007 char *uri;
008 char *proto;
009 } http_req;
010
011 int strncspn(const char *s, int max, const char *charset) {
012 int i, j, cs_sz;
013 char c;
014
015 cs_sz = strlen(charset);
016 for(i = 0; i < max && s[i] != 0; i++) {
017 c = s[i];
018 for(j = 0; j < cs_sz; j++) {
019 if(c == charset[j]) return i;
020 }
021 }
022 return max;
023 }
024
025 char *strndup(const char *s, int max) {
026 int sz = strlen(s);
027 char *buf;
028
029 if(sz > max) sz = max;
030 buf = (char *)malloc(sz + 1);
031 memcpy(buf, s, sz);
032 buf[sz] = 0;
033
034 return buf;
035 }
036
037 int strnchrs(const char *s, int max, int c, const char *chrs[], int chrs_max) {
038 int i, j = 0;
039
040 for(i = 0; i < chrs_max; i++, j++) {
041 for(; j < max; j++)
042 if(s[j] == c) break;
043 if(j == max) break;
044 chrs[i] = s + j;
045 }
046
047 return i;
048 }
049
050 int http_req_parse(http_req *req, const char *buf, int sz) {
051 int i;
052 const char *last;
053 const char *fss[3], *starts[3];
054
055 sz = strncspn(buf, sz, "\r\n");
056 last = buf + sz;
057
058 if(strnchrs(buf, sz, ' ', fss, 3) != 2)
059 return -1;
060
061 starts[0] = buf;
062 starts[1] = fss[0] + 1;
063 starts[2] = fss[1] + 2;
064 fss[2] = last;
065
066 for(i = 0; i < 3; i++)
067 if(starts[i] == fss[i]) return -1;
068
069 req->method = strndup(starts[0], fss[0] - starts[0]);
070 req->uri = strndup(starts[1], fss[1] - starts[1]);
071 req->proto = strndup(starts[2], fss[2] - starts[2]);
072
073 return 0;
074 }
075
076 int main(int argc, const char *argv[]) {
077 const char *data = "GET /test.html HTTP/1.1";
078 http_req req;
079
080 if(http_req_parse(&req, data, strlen(data)) < 0) {
081 fprintf(stderr, "error to parse request line!\n");
082 return 1;
083 }
084
085 printf("request line: %s\n", data);
086 printf("method: %s\n", req.method);
087 printf("uri: %s\n", req.uri);
088 printf("protocol: %s\n", req.proto);
089
090 return 0;
091 }
行數
表面上看來,改進可讀性之後,程式碼的行數突然大增。但是如果仔細算一下有意義的 statement 數目,其實是減少的。之所以會有大增的表象,是來自於 function 、 變數的宣告 、 呼叫和留白。但這些是否該例入程式的複雜度裡?本人採取否定的態度。我的計算方法,是將每一個 assign statement 算一行,每一個 for 、 if 、 break 、 return 、 continue 算一行。如此
if(...) break;
會算兩行。而 variable 和 function 的宣告不列入計算。
仔細算下來,最後一個程式比第一個程式還少上數行。
執行檔的大小
-rwxr-xr-x 1 thinker users 6117 May 6 00:06 readability0
-rwxr-xr-x 1 thinker users 6161 May 5 20:58 readability1
-rwxr-xr-x 1 thinker users 6064 May 5 21:30 readability2
-rwxr-xr-x 1 thinker users 6125 May 6 00:06 readability3
依序為前面四個程式的大小。第四個程式的成本是區區數個 bytes ,但可讀性卻大幅的改善。另一方面,本程式規模較小,大程式的的重複性更高,增加可讀性可能會使程式比原來更小。程式愈大,其效能愈大。
結論
程式的可讀性,來自於有意義的名稱、邏輯的分割。而可讀性卻不會增加程式的大小,反而能使程式更精簡、更小、甚至更快。本文並沒有討論到模組的計設和資訊封裝等議題,單就程式流程的可讀性進行討論。