程式碼的可讀性比較

以下就程式碼的可讀性即其產生之 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 ,但可讀性卻大幅的改善。另一方面,本程式規模較小,大程式的的重複性更高,增加可讀性可能會使程式比原來更小。程式愈大,其效能愈大。

結論

程式的可讀性,來自於有意義的名稱、邏輯的分割。而可讀性卻不會增加程式的大小,反而能使程式更精簡、更小、甚至更快。本文並沒有討論到模組的計設和資訊封裝等議題,單就程式流程的可讀性進行討論。