我們知道,int和double能表示的數值的範圍不同。其中,64位有符號整數的範圍是[-9223372036854775808,9223372036854775807],而64位無符號整數的範圍是[0,18446744073709551615]。這兩個區間有一定的overlap,而double可以表示的範圍更大。
現在,需要編寫兩個函式:給定一個double型的value,判斷這個value是否是一個合法的int64_t或者uint64_t。本文說的“合法”,是指數值上落在了範圍內。
1 2 3 |
bool is_valid_uint64(const Double &value); bool is_valid_int64(const Double &value); |
這裡我們用Double而不是double,原因是我們的double不是基礎資料型別,而是通過一定方法實現的ADT,這個ADT的成員函式有:
1 2 3 4 5 6 7 |
class Double { public: int get_next_digit(bool &is_decimal); bool is_zero(); bool is_neg(); }; |
通過呼叫get_next_digit
,可以返回一個數字,不斷呼叫它,可以得到所有digits。舉個例子,對於值為45.67的一個Double物件,呼叫它的get_next_digit
成員函式將依次得到
4 is_decimal = false //表示整數部分
5 is_decimal = false //表示整數部分
6 is_decimal = true //表示小數部分
7 is_decimal = true //表示小數部分
當get_next_digit
返回-1時,表示讀取完畢。
如何利用Double類裡的成員函式,來實現is_valid_uint64
和is_valid_int64
這兩個函式呢?
一些新手可能會寫這樣的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
bool is_valid_uint64(const Double &value) { bool is_valid = true; int digits[2000]; int counts = 0; if (value.is_zero()) { is_valid = true; } else if(value.is_neg()) { is_valid = false; } else { bool is_decimal = false; int digit = 0; while((digit=value.get_next_digit(is_decimal)) != -1) { if (is_decimal) { is_valid = false; break; } else { digits[counts++] = digit; } } uint64_t tmp = 0; uint64_t base = 1; for (int i = counts - 1; i >= 0; i++) { tmp += digits[i] * base; if (tmp > UINT64_MAX) { is_valid = false; break; } base *= 10; } } return is_valid; } bool is_valid_int64(const Double &value) { bool is_valid = true; int digits[2000]; int counts = 0; if (value.is_zero()) { is_valid = true; } else if(value.is_neg()) { bool is_decimal = false; int digit = 0; while((digit=value.get_next_digit(is_decimal)) != -1) { if (is_decimal) { is_valid = false; break; } else { digits[counts++] = digit; } } uint64_t tmp = 0; uint64_t base = 1; for (int i = counts - 1; i >= 0; i++) { tmp += digits[i] * base; tmp *= -1; if (tmp INT64_MIN) { is_valid = false; break; } base *= 10; } } else { bool is_decimal = false; int digit = 0; while((digit=value.get_next_digit(is_decimal)) != -1) { if (is_decimal) { is_valid = false; break; } else { digits[counts++] = digit; } } uint64_t tmp = 0; uint64_t base = 1; for (int i = counts - 1; i >= 0; i++) { tmp += digits[i] * base; if (tmp > INT64_MAX) { is_valid = false; break; } base *= 10; } } return is_valid; } |
這樣的程式碼,存在諸多問題。
設計問題
不難發現,兩個函式存在很多相似甚至相同的程式碼;而同一個函式內部,也有不少程式碼重複。重複的東西往往不是好的。重構?
效能問題
先獲得所有digits,然後從最低位開始向最高位構造值,效率較低。難道沒有可以從最高位開始,邊獲得邊計算,不需要臨時陣列儲存所有digits的方法嗎?
正確性問題
隨便舉幾個例子:
第24行,tmp += digits[i] * base
;有沒有考慮到可能的溢位呢?
第68行,難道有小數部分就一定不是合法的int64嗎?那麼,123.000?嗯?
規範問題
帥哥,這麼多程式碼,一行註釋都沒有,這樣真的好嗎?
因此,毫無疑問,這是爛程式碼,不合格的程式碼,需要重寫的程式碼。
以下是我個人認為比較好的設計和實現,僅供參考。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
bool is_valid_uint64(const Double &value) { bool ret = false; check_range(value, &ret, NULL); return ret; } bool is_valid_int64(const Double &value) { bool ret = false; check_range(value, NULL, &ret); return ret; } void check_range(const Double &value, bool *is_valid_uint64, bool *is_valid_int64) const { /* * 對於一個負數的value,它不可能是一個合法的uint64. * 因此,只剩下三種可能: * I 輸入的value是負數,判斷是否是合法的int64 * II 輸入的value是正數,判斷是否是合法的uint64 * III 輸入的value是正數,判斷是否是合法的int64 * 對於第II、III這兩種情況:只要判斷value的值是否超過uint64、int64的上界即可 * 對於第I種情況,我們利用-A > -B 等價於 A * 因此,在第I種情況裡,可以判斷value的絕對值,是否超過int64的最小值的絕對值即可。 * (int64的最小值的絕對值?那不就是int64的最大值?哦,不!) * 因此,不管哪種情況,判斷絕對值是否超過某個上界即可。 * 這三種情況,上界不一樣。把三個上界存到了一個二維陣列THRESHOLD裡 */ bool *is_valid = NULL; static const int FLAG_INT64 = 0; static const int FLAG_UINT64 = 1; static const int SIGN_NEG = 0; static const int SIGN_POS = 1; int flag = FLAG_INT64; if (NULL != is_valid_uint64) { is_valid = is_valid_uint64; flag = FLAG_UINT64; } else { is_valid = is_valid_int64; } *is_valid = true; if (value.is_zero()) { //do nothing。0是合法的uint64,也是合法的int64 } else { int sign = value.is_neg() ? SIGN_NEG : SIGN_POS; if ((SIGN_NEG == sign) && (FLAG_UINT64 == flag)) { *is_valid = false;//負數不可能是合法的uint64 } else { uint64_t valueUint = 0; static uint64_t ABS_INT64_MIN = 9223372036854775808ULL; //int64 uint64 static uint64_t THRESHOLD[2][2] = { {ABS_INT64_MIN, 0}, //neg {INT64_MAX, UINT64_MAX} }; //pos int digit = 0; bool is_decimal = false; while ((digit = value.get_next_digit(is_decimal)) != -1) { if (!is_decimal) { //為了防止溢位,我們不能這麼寫: //"value * 10 + digit > THRESHOLD[index]" if (valueUint > (THRESHOLD[sign][flag] - digit) / 10) { *is_valid = false; break; } else { valueUint = valueUint * 10 + digit;//霍納法則(也叫秦九韶演算法) } } else { if (!digit) { *is_valid = false; //小數部分必須是0;否則不可能是合法的uint64、int64 break; } } } } } } |
程式碼規範
團隊的程式碼規範,一般由領導和大佬們制定後,大家統一實行。這裡面有幾個問題:
真的需要程式碼規範嗎?
言下之意,制定和執行程式碼規範是否浪費時間?
答案是:It depends。如果專案很龐大、程式碼質量要求很高,那麼,制定和執行程式碼規範所花費的時間,將大大少於後期因為不規範開發帶來的種種除錯和維護成本。如果是小打小鬧的程式碼,就無所謂了。
程式碼規範的制定為什麼這麼難?
原因眾多,其中一個很重要的部分是團隊每個人的口味和觀點不盡相同。就程式碼風格而言,有人喜歡對內建型別變數i使用i++,有人堅持認為應該使用++i不管i是不是複雜型別。因此,制定程式碼規範需要在討論之後最後拍板決定,這裡面甚至需要獨裁!是的,獨裁!
程式碼規範制定需要注意什麼事項?
如果程式碼規範限制太鬆,那麼等於沒有規範;如果太嚴,大大影響開發效率。這裡面的尺度,需要根據專案需要、團隊成員特點全面考量,進行取捨。
需要注意的是,沒有任何一種程式碼規範是完美的。例如,在C++中,如果啟用異常,那麼程式碼的流程將會被各種異常處理中斷,各種try catch throw讓程式碼很不美觀;如果禁用異常,也就是在開發的過程中不能使用異常特性,那麼團隊成員可能因為長期沒有接觸這項語言feature而造成知識和技能短板。
程式碼風格舉例
舉兩個我認為比較重要、比較新鮮、比較有趣的程式碼風格。
1,使用引用需要判空嗎?
1 2 |
void f(int &p); void g(int *p); |
我們都知道,在g中,使用*p前需要對p是否為NULL進行判斷,那麼f呢?如果質量非常關鍵、程式碼安全非常重要的場景,那麼實際上,也是需要的。因為呼叫者可能這樣:
1 2 3 |
int *q = NULL; //...... f(*q); |
因此,需要在f裡增加if(NULL == &p)
的判斷。
2,級聯if else語句。
首先看一個我個人認為不好的程式碼風格:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int f(int a, int b) { if (a >= 1) { if (b >= 1) { if (a >= b) { //do sth } else { //error1 } } else { //error2 } } else { //error3 } } |
這個函式的核心在於do sth部分。其實我們可以改寫為級聯if-else形式,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int f(int a, int b) { if (a < 1) { //error3 } else if (b < 1) { //error2 } else if (a < b) { //error1 } else { //so, a>=1 && b>=1 && a>=b //do sth } } |
是不是優美多了?前面只做一些錯誤處理、前期準備、引數檢查等,最後的else分支做實實在在的功能性事情。
Code Review
什麼是Code Review?
很多人把它翻譯為程式碼審查,我覺得太政治味了。程式設計師尤其是新手寫完程式碼後,可能會有風格問題(比如不符合團隊的程式碼規範)、安全性問題(比如忘記指標判空)、優雅性問題(比如大量冗餘程式碼)、正確性問題(比如演算法設計錯誤),那麼在釋出程式碼到公共庫之前,提交給師兄或者mentor,讓他幫你review一下程式碼,並提出可能的問題和建議,讓你好好修改。這樣的過程,就叫做Code Review。
我的天吶,那這不是很佔用時間?
是的。一個寫程式碼,一個看程式碼,看程式碼的時間可能並不比全新寫一份程式碼少。那麼,這又是何必呢?
主要的原因有:
1,review確實佔用了開發時間,然而開發,或者說寫程式碼,其實只佔很少的時間比例。很多時間花在debug、除錯、寫文件、需求分析、設計演算法、維護等等上。
2,程式碼質量非常重要,這點時間投入是值得的。與其後期苦逼追bug,不如前期多投入點時間和人力。
3,培養新人,讓新手更快成長。
如何更好的執行Code Review
這裡給幾點建議:
1,不走過場。走過場,還不如不要這個流程。
2,作為Reviewer,看不懂程式碼就把作者拉過來,當面詢問,不要不懂裝懂,也不要愛面子不好意思問。
3,作為Coder,心裡要有感激之情。真的。不要得了便宜還賣乖,感恩reviewer,感激reviewer對自己的進步和成長所做出的貢獻,所花費的心血。中國人裡狼心狗肺、忘恩負義、不懂感恩的人還算少嗎?
4,作為Coder,給Reviewer Review之前,請先做單元測試並確保通過,並自己嘗試先整體看一遍自己本次提交的程式碼。注意,不要給別人提還沒除錯通過的程式碼,這是非常不尊重別人的表現。
質量保證
1,測試不是專屬QA的活兒,對自己寫的程式碼提供質量保證,是程式設計師的職責。QA要負責的,是系統的質量,不是模組的質量。
2,測試,需要意識,需要堅持。我發現C++程式設計師、前端程式設計師的測試意識或者說質量意識最強;資料科學家或者資料工程師的質量意識最差,很多人甚至不寫測試用例。當然,這不怪他們,畢竟,有時候程式碼裡有個bug,準確率和召回率會更高。
3,測試用例的編寫和設計需要保證一定的程式碼覆蓋率,力求讓每個分支和流程的程式碼都走到,然後分析執行結果是否是符合期望的,不要只考慮正確路徑上的那些分支。
4,測試用例的編寫和設計力求全面,考慮到方方面面。以非常經典的二分搜尋為例:
int binary_search(int *p, int n, int target, int &idx);
binary_search函式返回值為0表示成功執行,輸出引數idx返回target在有序陣列p中(第一次出現)的位置,-1表示不存在。
那麼測試用例至少應該涵蓋:
- p為NULL的情況
- 陣列大小n分別為負數、0、1、2時情況
- 陣列p不是有序陣列的情況
- target在陣列中出現0次、1次、n次的情況
你是否都考慮到了呢?
4,有時候,自己書寫測試用例顯得刀耕火種,現在已經有很多輔助的工具,讀者可以自行google一下。