C/C++ coding style 以及 clean code 教學 (基礎版)
目標:
- 幫助初學者們(特別指沒被code review,只寫過能動的code的人),了解coding style 跟 clean code 的好處
筆者背景:
- Synolgoy RD Intern
- LeetCode 500 題目左右 (主要easy 跟 medium)
- LeetCode Contest Rank 前 25% (knight badges)
- 2022 年底有面過台灣GOOGLE SWE Intern (Rejected)
緣起:
1.到Synology RD Intern 後 真正地感受到 coding style 跟 clean code 的重要性,想記錄下來並且幫助到有需要的人
進入正題:
我希望大家能帶走的點
- 如果你在function 中發現需要編寫冗長的代碼來完成某個任務,那麼應將其封裝為一個獨立的function。
- 利用好的變數和函數命名讓code更好讀
- 換行(new line) 的重要性
那我們就從 1 開始說起
以 LeetCode 752 題為例 (不需要會這題,請把重點著重在格式上)
這是沒有使用 將 冗長的代碼 封裝成獨立的function 的樣子
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
unordered_map<string, int> dead;
unordered_map<string, int> vis;
for (auto &s: deadends)
dead[s]++;
if(dead["0000"]) return -1;
// bfs
queue<string> q;
q.push("0000");
int step = 0;
while (!q.empty()) {
int size = q.size();
for (int i = 0; i < size; ++i) {
string pwd = q.front(); q.pop();
if (pwd == target) return step;
for (int i = 0; i < 4; ++i){
char org = pwd[i];
// get front
// get back
char front = (pwd[i] == '9') ? '0' : org+1;
char back = (pwd[i] == '0') ? '9' : org-1;
pwd[i] = front;
if (!dead[pwd] && !vis[pwd]) {
q.push(pwd);
vis[pwd] = 1;
}
pwd[i] = back;
if (!dead[pwd] && !vis[pwd]) {
q.push(pwd);
vis[pwd] = 1;
}
pwd[i] = org;
}
}
step++;
}
return -1;
}
};
這是有這樣做的樣子
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
recDeadEnds(deadends);
if (initImpossible()) {
return -1;
}
return bfs(target);
}
void recDeadEnds(vector<string>& deadends) {
for (auto &s: deadends)
block[s]++;
}
bool initImpossible() {
return block.find("0000") != block.end();
}
int bfs(string &target) {
queue<string> q;
q.push("0000");
int step = 0;
while (!q.empty()) {
int size = q.size();
for (int i = 0; i < size; ++i) {
string pwd = q.front(); q.pop();
if (pwd == target) return step;
insertNewPWD(pwd, q);
}
step++;
}
return -1;
}
void insertNewPWD(string &pwd, queue<string> &q) {
for (int i = 0; i < 4; ++i) {
char org = pwd[i];
// front case
pwd[i] = (org == '9') ? '0' : org + 1;
if (!block[pwd] && !vis[pwd]) {
q.push(pwd);
vis[pwd] = 1;
}
// back case
pwd[i] = (org == '0') ? '9' : org - 1;
if (!block[pwd] && !vis[pwd]) {
q.push(pwd);
vis[pwd] = 1;
}
pwd[i] = org;
}
}
private:
unordered_map<string, int> block;
unordered_map<string, int> vis;
};
看第一個例子我們會發現code 需要花時間讀懂,頂多只會注意到有使用bfs
相較之下看第二個例子我們會清楚地看到程式碼的架構
recDeadEnds()
initImpossible()
bfs()
...insertNewPWD()
依序執行 recDeadEnds(), initImpossible(), bfs() 這三個 function,
而bfs()裡面有可能會呼叫insertNewPWD()
基本上好讀非常多
接下來我們可以參考上面的例子來感受命名的好壞
對我來說命名的目標是
讓第一次看到這份code的人能夠看懂8成在幹嘛
在這邊大家可以先參考這篇文章
而就我個人經驗,我看過最多人使用的是
第一個字母小寫的駝峰式命名
在看到一些JavaScript的教學中很常看到
而在C++的業界中也很常看到 推薦給大家
而基本上命名其實可以舉出很多細節的例子,也可以探討很多地方
之後會獨立發一篇文章探討
接下來我們要探討 換行(new line) 的重要性
舉這個例子來說
int openLock(vector<string>& deadends, string target) {
recDeadEnds(deadends);
if (initImpossible()) {
return -1;
}
return bfs(target);
}
我在recDeadEnds(), initImpossible() 跟 bfs() 三個function間隔中
都有刻意換行
原因是因為我想表達我在這個函數中
呼叫了三個function
做了三件事情
如果今天我要在一個超過百行的程式碼debug時
藉由清楚地把架構寫出來
我能夠更清晰地去分析問題出在哪
這是很重要的細節
當有一天你遇到超過百行的code時
你就可以明顯感受到這樣分行的好處
搭配 1 的 技巧
它會讓你很方便地去debug 思路
而當你遇到上千行以上的程式碼時
這樣做已經是一件必須的事情了
最後在幫大家做一個重點複習
以後寫程式記得
- 遇到冗長的code 包成 function
- 多花時間用心想怎麼命名
- 善用換行表達自己的思路