往期回顧
TALK 6:編程的技術|藝術|術術(上篇)骨灰級程序員的心路歷程
前面兩篇里,骨灰級程序員梁峻墅給大家介紹了他的心路歷程,他談了程序員文化和武林文化的理解,將編程與孫子兵法對照,闡釋編程的藝術性表達以及哲學思考。本篇將不再務虛,而是直接上代碼,讓梁老師帶著你解讀牛逼代碼的高明之處。
務虛的事都講完了,現在得真的要講講務實的事了。前面講的那些是武功秘籍的目錄,而真正的武功秘籍在代碼里。實踐出真知,只有虛實結合,才能感同身受。
我找了一段Zero-Day(編者注:下文簡稱0day)組織幾乎每個程序都要用到的一段代碼作為示例。
0day,用過盜版軟件的朋友應該都很熟悉,它是全球最牛B的盜版組織,里面高手如云,都是Richard Stallman的追隨者。任何一個被他們盯上的大廠軟件,只要敢早上發布,中午的發布會招待宴還沒吃完,破解版就已經在各大盜版網站上可以下載了,平均破解時間就是兩三個小時,承諾破解時間不超過24小時,所以叫0day,當天解決,童叟無欺。我們就來看看這些全球頂尖黑客是怎么寫代碼的。
我找的這段代碼的功能很簡單,就是一個基于文件的記錄日志類,其C++版本加上頭文件,總代碼行數不超過200行,而核心代碼不到100行,但就在這方寸之間,隱藏著十一個戰術思想,三個戰略思想,還有三個核彈級思想。就是個日志文件功能,如果是你設計,能有什么想法?而往往是簡單中蘊含的偉大,才能更加讓人震撼。現在咱們就按圖索驥,開始一段與頂尖高手同行的代碼探險之旅。
這段代碼是個標準的C++類,為方便演示,我使用的是其Windows平臺的版本,此類可以在所有Visual Studio的C++應用中使用,就兩個文件:LogFile.h和LogFile.cpp。
可以先總覽一下:
LogFile.h
// Log.h: interface for the CLog class.
//
//////////////////////////////////////////////////////////////////////
class CLogFile {
public:
CLogFile(LPCTSTR pszPathName4User = _T(""));
virtual ~CLogFile();
bool Record(LPCTSTR pszFormat, ...);
bool SetPathName4Host(LPCTSTR pszPathName4Host);
bool SetPathName4User(LPCTSTR pszPathName4User);
bool SetFileName4Host(LPCTSTR pszFileName4Host);
bool SetFileName4User(LPCTSTR szFileName4User);
bool SetHeader(LPCTSTR szHeader);
LPCTSTR GetPathName4Host();
LPCTSTR GetPathName4User();
LPCTSTR GetFileName4Host();
LPCTSTR GetFileName4User();
LPCTSTR GetPathName();
LPCTSTR GetFileNameFullPath();
protected:
SYSTEMTIME m_tSystemTime;
TCHAR m_szPathName4Host[MAX_PATH + 1];
TCHAR m_szPathName4User[MAX_PATH + 1];
TCHAR m_szFileName4Host[MAX_PATH + 1];
TCHAR m_szFileName4User[MAX_PATH + 1];
TCHAR m_szPathName[MAX_PATH + 1];
TCHAR m_szFileNameFullPath[MAX_PATH + 1];
TCHAR m_szHeader[MAX_LENGTH_CONTENT_PER_LINE + 1];
TCHAR m_szLine[MAX_LENGTH_CONTENT_PER_LINE + 1];
bool IsPathOrFileExist(LPCTSTR pszPathOrFileName);
bool BuildFilePath();
bool BuildPathAndFilePath();
};
// Log.cpp: implementation of the CLog class.
//
//////////////////////////////////////////////////////////////////////
CLogFile::CLogFile(LPCTSTR pszPathName4User) {
ZERO_MEMORY(m_szPathName4Host);
ZERO_MEMORY(m_szPathName4User);
ZERO_MEMORY(m_szFileName4Host);
ZERO_MEMORY(m_szFileName4User);
ZERO_MEMORY(m_szPathName);
ZERO_MEMORY(m_szFileNameFullPath);
ZERO_MEMORY(m_szLine);
ZERO_MEMORY(m_szHeader);
_tcscpy_s(m_szHeader, MAX_LENGTH_CONTENT_PER_LINE, _T("F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15"));
::GetModuleFileName(NULL, m_szFileNameFullPath, MAX_PATH);
_tsplitpath(m_szFileNameFullPath, m_szPathName4Host, m_szPathName, m_szFileName4Host, NULL);
_tcscat_s(m_szPathName4Host, MAX_PATH, m_szPathName);
if (pszPathName4User) {
if (*pszPathName4User) {
_tcscpy_s(m_szPathName4User, MAX_PATH, pszPathName4User);
}
else {
*m_szPathName4User = _T('.');
_tcscat_s(m_szPathName4User, MAX_PATH, m_szFileName4Host);
}
}
BuildPathAndFilePath();
}
CLogFile::~CLogFile() {
}
bool CLogFile::BuildFilePath() {
GetLocalTime(&m_tSystemTime);
int iReturn = _sntprintf(m_szFileNameFullPath, MAX_PATH, _T("%s%s.%s.%04d%02d%02d.%p.txt"),
m_szPathName, m_szFileName4Host, m_szFileName4User,
m_tSystemTime.wYear, m_tSystemTime.wMonth, m_tSystemTime.wDay, this);
return 0 < iReturn;
}
bool CLogFile::BuildPathAndFilePath() {
bool bReturn = 0 < _sntprintf(m_szPathName, MAX_PATH, _T("%s%s"), m_szPathName4Host, m_szPathName4User);
if (bReturn) {
bReturn = BuildFilePath();
}
return bReturn;
}
bool CLogFile::Record(LPCTSTR pszFormat, ...) {
int iReturn = 0;
FILE* pFile = NULL;
do {
if (!IsPathOrFileExist(m_szPathName)) {
break;
}
if (!BuildFilePath()) {
break;
}
bool bIsNew = !IsPathOrFileExist(m_szFileNameFullPath);
pFile = _tfopen(m_szFileNameFullPath, _T("a"));
if (!pFile) {
break;
}
if (bIsNew) {
iReturn = _ftprintf(pFile, _T("Time User %s
"), m_szHeader);
if (0 >= iReturn) {
break;
}
}
va_list vlArgs;
va_start(vlArgs, pszFormat);
iReturn = _vsntprintf(m_szLine, MAX_LENGTH_CONTENT_PER_LINE, pszFormat, vlArgs);
va_end(vlArgs);
if (0 > iReturn) {
break;
}
iReturn = _ftprintf(pFile, _T("%02d:%02d:%02d.%03d %s %s
"), m_tSystemTime.wHour,
m_tSystemTime.wMinute, m_tSystemTime.wSecond, m_tSystemTime.wMilliseconds,
m_szFileName4User, m_szLine);
} while (false);
if (pFile) {
fclose(pFile);
}
return 0 < iReturn;
}
bool CLogFile::IsPathOrFileExist(LPCTSTR pszPathOrFileName) {
return (0 == _taccess(pszPathOrFileName, 0));
}
bool CLogFile::SetPathName4Host(LPCTSTR pszPathName4Host) {
_tcsncpy(m_szPathName4Host, pszPathName4Host, MAX_PATH);
return BuildPathAndFilePath();
}
bool CLogFile::SetPathName4User(LPCTSTR pszPathName4User) {
_tcsncpy(m_szPathName4User, pszPathName4User, MAX_PATH);
return BuildPathAndFilePath();
}
bool CLogFile::SetFileName4Host(LPCTSTR pszFileName4Host) {
_tcsncpy(m_szFileName4Host, pszFileName4Host, MAX_PATH);
return BuildFilePath();
}
bool CLogFile::SetFileName4User(LPCTSTR szFileName4User) {
_tcsncpy(m_szFileName4User, szFileName4User, MAX_PATH);
return BuildFilePath();
}
bool CLogFile::SetHeader(LPCTSTR szHeader) {
_tcsncpy(m_szHeader, szHeader, MAX_PATH);
return true;
}
LPCTSTR CLogFile::GetPathName4Host() {
return m_szPathName4Host;
}
LPCTSTR CLogFile::GetPathName4User() {
return m_szPathName4User;
}
LPCTSTR CLogFile::GetFileName4Host() {
return m_szFileName4Host;
}
LPCTSTR CLogFile::GetFileName4User() {
return m_szFileName4User;
}
LPCTSTR CLogFile::GetPathName() {
return m_szPathName;
}
LPCTSTR CLogFile::GetFileNameFullPath() {
return m_szFileNameFullPath;
}
先從戰術思想談起,有十一個,容我逐一道來。
戰術思想一:全名命名規則
代碼的第一眼感覺,沒有注釋!這幫高手果然很清高啊,他們不會幫助你看懂,因為這個世界上總有人不配看懂。只能沉下心來,自力更生了。再仔細看代碼,會發現代碼中的變量名、方法名都很長。第一個戰術級思想浮出水面:全名命名規則。命名使用單詞全名,而很多程序員喜歡使用縮寫,而縮寫并不一定能與所有人達成共識,導致命名的意義大打折扣。良好的命名可以代替注釋,且效率更高。微軟的函數命名平均長度是13個字母,而0day代碼中的命名平均長度是16.8個字母,超過微軟水準將近30%。可以說,命名平均長度能夠作為代碼段位的參考之一。
戰術思想二:前綴命名規則
再仔細看每一個命名,第二個戰術級思想浮出水面:前綴命名規則。所有的變量名都有類型前綴,如字符串變量的前綴是“sz”,字符串指針變量的前綴是“psz” ,整型(int)變量的前綴是“i” ,布爾型(bool)變量的前綴是“b”,這些類型前綴雖然使用了縮寫,但這些縮寫都是C/C++程序員所共識的。還有,所有的類成員級變量在類型前綴前再加上“m_”前綴指示作用域,m是member的縮寫,其實還有一個“g_”前綴代表全局作用域,但全局變量只有在C代碼中很常見,而在C++代碼中幾乎從不使用。
戰術思想三:名詞前置命名規則
再再仔細看每一個命名,第三個戰術級思想浮出水面:名詞前置命名規則。例如類成員字符串變量“宿主路徑名”命名為m_szPathName4Host,“用戶路徑名”命名為m_szPathName4User,如果按人類正常思維應該是m_szHostPathName和m_szUserPathName才對,但他們卻名詞前置,形容詞動詞后置。其目的是為了給相關命名進行分類,最早是為了能在代碼統計工具的報告中,能把相關命名在α排序中排在一起,以便進行代碼分析;而在后來的現代IDE的代碼編輯器中,都有自動完成功能,根據輸入的部分字母自動提示可能的輸入,按名詞前置命名規則,提示內容將把相關命名排在一起,便于程序員選擇。如鍵入“m_szP”,將提示出m_szPathName4Host和m_szPathName4User,方便程序員在使用相關變量或方法時提高效率。
戰術思想四:介詞縮寫命名規則
在上面提到的命名中,都有一個阿拉伯數字4,這是什么鬼?第四個戰術級思想浮出水面:介詞縮寫命名規則。用4的英文諧音代替介詞“for”,原命名應為m_szPathNameForHost,介詞作為前置命名分類與后置形容詞、動詞的分界線被大量使用,為節約鍵擊次數而在組織內約定的縮寫。類似還有2,諧音英文的“to”,因為在程序中各種轉換也非常多,如BinToHex(二進制轉十六進制),可以縮寫為Bin2Hex。這可以理解為長命名思想與少鍵擊思想的辯證統一。
戰術思想五:對稱命名規則
看上面的成員變量和對應的設置方法名和獲取方法名,第五個戰術級思想浮出水面:對稱命名規則。如此整齊劃一的命名,不但能幫助閱讀者在沒有注釋的情況下快速理解各方法的意圖,還能讓使用者無需翻看源碼就能準確調用。
大家看看,一個小小的命名,已經是殺機四伏,下足了功夫。十一個戰術思想接近一半,都是在談命名。就是因為命名是代碼的基石,它是多米諾骨牌效應里的第一塊骨牌,每塊磚不做好,將會影響整個大廈的安危。這些命名規則的終極目標都是為了用空間換時間。在你的每一次鍵擊中,每個思想可能只為你節約了0.1秒,但經不住長年累月的積累,你的有效編程時間就是比別人多,還沒開始比賽,你就已經勝過了。
戰術思想六:使用制表符縮進
代碼中還有一個不易察覺的細節,其代碼縮進使用的是制表符(TAB鍵),第六個戰術級思想浮出水面:使用制表符縮進。關于縮進使用制表符還是空格,業界一直爭論不斷,且沒什么定論,主要原因就是覺得這是個小問題,無傷大雅,大家隨意,開心就好。但這些頂尖高手只用制表符,原因很暖心,僅僅是為了尊重同行!制表符最早出現是為了控制打印機在打印時的左邊距,當時定義為8個空格,可視化編程出現后才用于代碼縮進,但當時顯示器的分辨率是320*200,一行最多顯示80個字符,這8個空格實在是太長了,于是就在編輯器中定義為4個空格,但后來有人覺得2個才好,還有人覺得1個更好,最后干脆作為編輯器配置項,根據喜好自定義吧。所以使用制表符縮進的代碼在編輯器中的顯示樣式將會符合當前使用者的習慣,而使用空格縮進的代碼將可能會導致當前使用者不適。多么細致的人文關懷,面向人性編程,面向開發者編程,時刻謹記。
戰術思想七:調用必須有返回值
觀察代碼中的每一個方法,發現都有返回值,哪怕是返回固定值!
第七個戰術級思想浮出水面:調用必須有返回值。絕大多數編程語言都允許調用沒有返回值,但這幫頂級精英為什么在可以用這個規則的情況下還是不用呢?這就是接口的藝術,為了向下兼容,未雨綢繆,面向未來編程!因為誰也無法預測,隨著代碼的不斷迭代,這個方法的使用條件可能會發生變化,而有返回值的調用是可以兼容沒有返回值的調用的,這樣可保持接口的歷史一致性,進退自如。這樣的設計一旦在public調用中發揮過一次作用,可就不是節約0.1秒的事了。
戰術思想八:減少嵌套深度
上面是Record方法中的一段代碼,使用了一個do-while循環語句,但循環條件是個固定布爾值false,意味著這個循環永遠只會執行一次,但為什么還要用循環語句呢?如果不用循環語句,正常的寫法應該是這樣的:
第八個戰術級思想浮出水面:減少嵌套深度。嵌套深度決定了人類大腦的思考深度,而思考深度則決定了消耗的能量和思考的難度。所以嵌套深度較低的代碼,讓人思考起來會比較輕松且不易出錯,而重度嵌套的代碼則更容易讓人疲倦且增加產生bug的幾率。
戰術思想九:辯證使用goto
這段代碼使用do-while循環語句的結構,并配合break語句來減少邏輯嵌套。第九個戰術級思想浮出水面:辯證使用goto。break語句的本質是goto語句,只是受限而已。而goto語句在早期面向過程編程的時代,由于其高效的操作效率而被濫用,把代碼寫的像面條一樣,扯不清,理還亂,這導致了上世紀60年代的軟件危機,并最終引發了軟件工程革命。在面向對象編程的時代,業界統一的共識是禁止使用goto。但goto語句的操作效率確實很高,所以善用break這種閹割版goto可以起到魚與熊掌兼得的效果。
戰術思想十:同一函數代碼不要跨屏
觀察Record方法的代碼行數達到36行,但業界一般的說法是每個函數的代碼行數不要超過30行,理由是人類的腦容量問題。但0day的判斷標準是,第十個戰術級思想浮出水面:同一函數代碼不要跨屏。只要任意函數的所有代碼在當前流行屏幕尺寸大小下能夠完全顯示即可。理由是只要整個代碼邏輯在人的靜態目視范圍之內,程序員的腦容量都夠用。除了靠減少代碼行數來防止縱向滾動屏幕,前面說的減少邏輯嵌套還能防止橫向滾動屏幕。代碼邏輯禁止跨屏規則能在很大程度上降低bug產生的幾率。
再觀察Record方法,發現一段有趣的代碼:
戰術思想十一:盡量使用順序代碼結構代替判斷代碼結構
BuildFilePath方法用于構造日志文件名,其中使用了系統日期作為文件名的一部分,目的就是把日志文件按天分隔,以防止文件過大。這意味著每次寫日志,都應判斷是否該更換文件名,但這種更換每天只發生一次。而這段代碼并沒有根據日期是否更改而構造文件名,而是每次都按當前日期構造文件名,這意味著文件名在一天內的調用中都是重復構造相同的文件名,這不是做無用功嗎?第十一個戰術級思想浮出水面:盡量使用順序代碼結構代替判斷代碼結構。判斷語句是bug產生的源泉,盡量不要使用,哪怕代碼看上去有點愚蠢。不認同的人可以試試,使用判斷語句來修改這段代碼,讓其看起來更有效率。當你被源源不斷的bug改到懷疑人生時,你才能真切地體會到這個思想的精妙之處。對這個未知世界,心存敬畏,才能保你福如東海,壽比南山。
前面談到了十一個戰術思想,每一個戰術思想,可能看過來都不復雜,但偉大往往都藏在細節中。十一個戰術思想,處處都在體現著:以空間換時間,面向人性編程,面向開發者編程,面向未來編程,以及對這個未知世界心存敬畏。接下來我們談談戰略級思想。
戰略級思想一:贈人玫瑰,手有余香
再觀察Record方法的定義,使用了非常罕見的不定長參數,第一個戰略級思想橫空出世:贈人玫瑰,手有余香。作為一個日志文件類的主要方法,通常就是把傳入的字符串參數,存儲到日志文件里就好了。為什么要使用一個非常冷門的技術?原因就是尊重傳統,方便你的同行,讓調用者更干、更爽、更安心。如果參數是一個字符串,則意味著調用方必須在調用此方法前,拼裝好字符串:
使用不定長參數,則可以這樣調用:
一行搞定!把方便留給別人,把困難留給自己,雷鋒精神時刻謹記。面向人性編程,面向開發者編程,面向開源編程。
戰略級思想二:簡單通用
通覽整體代碼,系統調用只使用過一次Windows API,其余均使用C運行時庫函數,第二個戰略級思想橫空出世:簡單通用。作為一個工具類,會被廣泛使用,包括跨平臺應用。如果使用Windows API,此類要移植到Unix/Linux平臺上將付出巨大代價。而C運行時庫函數是語言標準而非平臺標準,在功能表現上所有平臺都是一致的,所以移植成本要低的多。而且代碼中還使用了C運行時庫函數的自適應字符集宏定義版本,使得此工具類無論編譯目標應用是MBCS字符集還是Unicode字符集都無需修改一行代碼!事實上,此工具類在組織內不但有多平臺版本,甚至還有多語言版本,包括C#、java、VB等。受益于使用語言標準的設計思想,各語言、平臺版本的代碼一致性很高,產生bug的幾率很小,移植成本非常低。
現在我們正式開始通過瀏覽代碼來理解代碼邏輯,先看類構造函數:
戰略級思想三:默認值的藝術
這個初始化還是相當復雜的,對關鍵類成員變量的默認值進行了規劃和設計,第三個戰略級思想橫空出世:默認值的藝術。為便于理解程序的設計思想,我寫了一個測試程序,使用不同的參數調用構造方法,然后調用對應的成員變量獲取方法,以查看成員變量的內容:
從以上結果可以看出,構造函數通過傳遞不同的參數,將成員變量初始化為不同使用理念的數據套。目的就是讓調用者在構造完類后,即可使用Record方法開始記錄日志,而無需任何配置!還是那句老話:把方便留給別人,把麻煩留給自己,雷鋒精神時刻謹記。
三個戰略級思想,以小搏大,已經從簡單到人性上升到墨家的兼愛和利他主義,這其實就是面向開源的編程思想內核。下面我再談三個核彈級思想。
核彈級思想一:即時熱調試
繼續仔細研讀測試結果,可了解代碼初始化意圖:給構造函數傳參空指針(NULL),則日志文件路徑自動配置為當前可執行文件路徑,緊接著調用Record方法即可產生日志文件,nice!如果給構造函數傳參非空字符串,如示例中是“log”,則自動配置日志文件路徑為當前可執行文件路徑后再附加“log”路徑,enn…如果傳參是空字符串或不傳任何參數(這是默認情況,應該是該類建議的主要使用方式),則自動配置日志文件路徑為當前可執行文件路徑后再附加帶前綴“.”的不包括擴展名的可執行文件名,what?
代碼是看懂了,但為啥?難道要自動創建如此詭異的路徑?但在Record方法中,不但沒有找到創建路徑的方法,還看到了這樣一段代碼:
這段代碼的意思就是當日志文件路徑不存在時,將退出Record功能,什么也不干!這個類的作用不就是記錄日志嗎?居然在某些情況下還不應記錄?What the fuck!
第一顆核彈君臨天下:即時熱調試。像C/C++這種接近硬件底層的編譯型語言,預定義有兩種編譯應用的形態:debug版本和release版本。debug版本用于在開發環境中調試,尤其是單步調試功能可以解決硬核的技術問題,而release版本用于正式發布,沒有調試功能。但代碼調試時,除了技術問題,還有更大量的業務邏輯問題需要調試。如果使用單步調試效率太低了,所以絕大多數C/C++程序員在debug版本中通過輸出日志調試業務邏輯。這些日志通過宏定義控制只在debug版本中編譯,而在release版本中忽略,因為正式發布的軟件不能在用戶方產生大量調試日志,否則日積月累會塞滿用戶的存儲空間。但是,誰也不能保證在debug版本中能調試完所有的業務邏輯問題,如果在用戶方部署的release版本出錯,大家束手無策。在互聯網發明以前,這個問題到也不太重要,因為即使在用戶方發現程序錯誤,程序員也沒辦法到達現場解決問題。但現在的互聯網技術可以支撐遠程登錄服務器或者個人計算機,賦予了技術支持人員可以在任何時間、任何地點、使用任何設備到達錯誤現場的能力,但老舊的編譯時debug和release機制,在新時代下也沒什么卵用。0day的精英們與時俱進,設計了這個動態debug和release機制:如果在當前可執行文件的目錄下,存在一個特別指定的目錄,則程序進入debug狀態,并在那個目錄下生成日志;否則程序保持release狀態,不輸出日志。牛B的思想閃耀星空!把代碼的debug和release狀態確認由編譯時后移到運行時,這意味著當程序發生業務邏輯問題,程序員可直接登錄到現場,程序都不用重啟,直接建立指定目錄,即可知道當前程序正在干什么,找到問題后,再把目錄一刪,揮一揮衣袖,不帶走一片云彩!
這個頂級設計還有一些非常貼心的細節設計,第一是關于那個指定的目錄。默認是不包括擴展名的當前可執行文件名,前面還有一個“.”。這是為了保持跨平臺操作的一致性,因為Unix/Linux平臺下的可執行文件沒有擴展名,如果單純使用當前可執行文件名,則因為重名而無法創建目錄,所以前面加個“.”來保證不重名,還順便成為隱藏目錄,因為Unix/Linux的文件系統定義以“.”開頭的目錄或文件具備隱藏屬性。雖然Windows平臺下不存在這些問題,但0day的絕大多數精英都是Windows平臺和Unix/Linux平臺雙料王牌,經常需要在多平臺間切換工作,為保持操作一致性,只好委屈一下Windows平臺了。但也無需焦慮,這個指定目錄可以在初始化或運行時隨便修改。修改指定目錄還有一個使用技巧,比如在同一目錄下有A1,A2,A3,B1,B2共5個應用程序,其中A1,A2,A3是有鉤稽關系的第一組應用,B1,B2是有鉤稽關系的第二組應用,可以設計為建立A目錄,則在A目錄中同時產生A1,A2,A3的日志,建立B目錄,則在B目錄中同時產生B1,B2的日志,達到相關應用群日志自動分組的目的。
第二是關于日志文件名。整個文件名分為5個部分:第一部分是應用程序名,這個很容易理解,一看就知道這個日志是哪個應用產生的;第二部分是一個自定義的名字,這個作用比較硬核,咱門后面再講;第三部分是日志產生的日期,為了防止文件過大,每個應用程序每天只有一個日志文件;第四部分比較特殊,是運行時日志文件類實例的內存地址,what?這能干啥用?使用實例的內存地址意味著每次啟動這個類,文件名就會發生變化,可用于指示這個應用程序在這個日期下的不同啟動批次。第五部分是固定擴展名“txt”,指示系統可用文本編輯器打開此文件。整個設計考慮了使用上的方方面面,盡量讓使用者更方便、更舒適,愛心媽媽,呵護全家。面向人性編程,面向開發者編程,面向開源編程。
核彈級思想二:統計日志
再看向文件寫入內容的代碼中使用制表符“ ”作為輸出內容的分隔符,第二顆核彈石破天驚:統計日志。為了能理解這個設計,咱們先看看調用方是如何使用這個類的,典型調用像這樣:
意圖就是把需要輸出的狀態、數據,如調用的方法名、錯誤描述等,組織成類似表格字段的方式分隔輸出。使用制表符可以保證用表格軟件打開日志文件或把文本復制到表格軟件里,效果是這樣的:
在第一個核彈的淫威下,再加上日志的記錄時間精確到毫秒的加持,程序員們徹底放開了,寫日志跟不要錢似的,瘋狂輸出,幾乎每個函數在返回前都會把當前處理結果輸出到日志里,面對這樣的海量日志,用眼睛找bug會看瞎的。所以創造性的利用表格的相關排序、分類匯總、透視圖等統計功能,快準狠地定位查找目標。比如示例那個日志,用透視圖看是這樣的:
或者是這樣的:
就說你想查啥?咋樣都行,就是拖拖拽拽的事。bug往哪里躲?它太難了…羽扇綸巾,談笑間,強擼灰飛煙滅。
核彈級思想三:調試多線程
還記得前面講到文件名的第二部分嗎?就是代碼里的成員變量m_szFileName4User,它是干什么用的?看遍代碼的上上下下,也看不出個所以然。我們對其賦值“robot”,看看出現啥情況:
日志文件名第二部分變成“robot”,日志文件中user列里面填充“robot”,仍然一頭霧水!第三顆潛射核彈韜跡隱智:調試多線程。多線程調試是程序員的噩夢,因為人類的大腦無法精確模擬計算機多線程的運行過程。所以多線程程序所產生的bug,尤其是無法必現的bug,常常讓人束手無策。在前面兩顆核彈的加持下,給解決這個問題帶來了希望。如果在日志文件類中加入同步機制,多個線程共享同一個日志文件類實例,則會導致多線程程序在調試狀態下被強制串行化為單線程程序,由于運行環境的變化很可能觸發不了那個多線程bug,所以每個線程必須單獨使用各自的日志文件。在分析時,將相關的所有線程日志全部拷貝到一個表格文件中,利用排序功能就能知道每個時刻,各個線程都正在干什么。這個user列就是用于區分這條記錄來自于哪個線程。多線程調試就這么被輕松地搞定了!說了那么多偉大,我都厭倦了:“老婆,快出來看上帝。”
核彈級思想是高手隱藏在編程中的不容易直接悟出來的彩蛋。但你一旦悟出來,一定會有醍醐灌頂的暢快淋漓。
我已把這段代碼上傳到gitee,訪問地址https://gitee.com/aeye/CTools/,歡迎大家共建,也可應用于項目中,給大家在代碼的海洋中探險時提供一把趁手的兵器。
最后,希望同學們也能夠創造出有思想,有靈魂,舉手投足之間都透露出優雅的代碼:
<本文完>
原文標題:河套IT TALK——TALK 12:編程的技術|藝術|術術 下篇:對著代碼解讀編程的哲學
文章出處:【微信公眾號:開源技術服務中心】歡迎添加關注!文章轉載請注明出處。
-
開源技術
+關注
關注
0文章
389瀏覽量
7928 -
OpenHarmony
+關注
關注
25文章
3716瀏覽量
16274
原文標題:河套IT TALK——TALK 12:編程的技術|藝術|術術 下篇:對著代碼解讀編程的哲學
文章出處:【微信號:開源技術服務中心,微信公眾號:共熵服務中心】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論