The two most important days in your life are the day you were born and the day you find out why. -Mark Twain

顯示具有 程式設計 標籤的文章。 顯示所有文章
顯示具有 程式設計 標籤的文章。 顯示所有文章

#84 程式設計的簡約格式 - 減少程式碼縮排


最近在 code review 的過程中,總是常常會看到一些令人驚喜的事情,其中一件事情和程式的寫法有關.有時候是邏輯可以在簡化,有時候是 coding style 上可以在簡化以便閱讀.首先來看 coding style 可以簡化的部份.在寫程式的過程中,有時會因為業務邏輯面的複雜而造成會寫出多個 nested if statements 的情況,如下面的例子



上面的程式碼其實沒錯,也算方便閱讀.也許是我個人的因素,遇到 nested if 的時候,我會儘量地朝向少點縮排的方式前進. 除此之外,第 7 行和第 13 行是回傳一樣的答案.在程式寫法上可以將他們結合在一起,因為那是一個 "業務邏輯",一般來說,我們希望同一件事情只要發生在一個地方.因此,可以將上述的程式碼改成如下:



一個 if 配合上多個檢查條件,我自己覺得這樣閱讀起來比上述的語法好些.

再來看另外一個例子.

原本的程式碼如下:



同樣的,這程式也沒錯.不過,閱讀起來有點 "階梯".因為當我們閱讀時,大腦會產生那種 "階梯",如果 if true part/false part 的內容物很多程式碼,我們還得仔細看縮排才知道下一行要跑到那繼續看.

其實,只要動一點腦筋想一下就可以把它寫的更簡單,如下



這樣的寫法是一樣的邏輯,而且是不是覺得更方便閱讀呢 ?

在寫程式的過程式,會常用到 if..else.. 是很正常的事情,然後這也代表你的業務邏輯將分成兩塊,而這兩塊的內容是獨立而不會互相干擾的,所以這兩塊的內容基本上不會重覆.若有重覆,那就是一般所謂的 duplicated code bad smell.另外,利用減少 "縮排" (indentation) 的技巧也能讓程式碼達到更方便閱讀的地步.
Share:

#80 寫程式的參考準則 (coding guideline) - C# 篇

曾有一些朋友問我,在微軟公司裡是否有寫程式的準則 (coding guideline).這件事因不同的團隊而異,大部份的團隊都會依循 MSDN 文件裡的建議,但並非每一個團隊都有文件記錄這些準則.以前我在 Windows 部門裡的某個團隊就正好有文件說明 C# coding guideline.除了 C# coding guideline 以外,還有其他的文件,例如 code review 文件, database 開發文件等等.在這篇文章中,我將從 C# coding guideline 開始寫起.這些 coding guideline 不是什麼秘密,很多都是來自 MSDN 的文件.若你的團隊也需要一份 C# coding guideline, 希望能派的上用場.

1. 在一份 C# 原始碼裡,別有 Tab , space 並存.這一個可以善用 Visual Studio 的編輯器設定幫你解決.一般來說,我們用 space 來取代 tab,例如一個 tab 等於四個 spaces.

2. 一行程式碼的字數通常設定在 120 個字元左右.為了方便閱讀,若有一個 api 有多個參數需要傳入,將多個參數分行排列,如下例


3. 有關設定變數 (variable),盡可能地將其 scoping 範圍找小一點的.

4. 變數宣告時不一定要馬上給初始值 (initialize).用到此變數時在給即可.若需要給一個初始值,請給一個有意義的值做為初始值.這邊所謂有意義的初始值不代表該資料結構的預設值.如 int count = 0; ,這是多此一舉的行為.

5. Global field 前面加上 _ .若是 local variable,則不用.
如 int _memberCount;  
     int memberCount;

6. 相同型別的變數要分行宣告.別擠在一行一起宣告,應分行.如:
int foo;
int bar;
int baz;

7. 對於常數型的變數 (內容不會變動) 記得使用 const 宣告. 如:
private const int _defaultCount = 100;

另外,對於這類常數型變數,最好能加上一個 comment 用來說明為何選用此內容. 如:
// 100 is chosen because of the size limit of the queue
private const int _defaultCount = 100;

8. 對於多個性質相關的常數型變數,可以考慮將他們整合在一個 static class 裡宣告並使用,如:

可改成


9. 用 string.Empty 來取代 ""

10. 用 string.IsNullOrWhiteSpace() 來檢查字串是否為 null 或是空白.

11. 對於一些 “應用值”,不應該 “hard-code”,而是該透過其他較彈性的方式取得 (像 Configuration).如
網路連線重試次數,timeout 時間, thread 數量, 資料庫連線字串等等

12. 在 if , while , for, foreach, return 前多個空行,以便於閱讀.

13. Class 裡的 method 之間有一個空白行.

14. 若有需要,善用 #region, #endregion 將相關的程式放在同一個區塊裡.同時 #region 之間也該有一個空白行.

15. 善用 IDE 裡的編輯器幫助你處理好空白的事情.如


16. 遇到一些特別情況時,善用 comment 以便未來工作.寫 comment 時不用一行一行寫 (inline),請在適當地方用一塊區域來說明程式的運作目的.


17. 對於 public method 都該檢查輸入參數是否符合你的預期.如:


18. 該有括號的地方都打上去,別偷懶不打.如:


19. public method, property 等一律使用 PascalCasing ,而非 public 改用 camelCasing. 如:
public int GetMyReward();
public string MyName {get; set;}

private int getMyReward();
private string _myName;

20. 為 variable, property, method, class, interface 取名是件重要的事情.取一個完整的名稱,別用簡稱.如 使用 GetConsoleWindow() 而不用 GetConWin()

21. Namespace 命名以 PascalCasing 方式,名字應以名詞為主.如: System.Security

22. Class 命名以 PascalCasing 方式,名字應以名詞為主,如 StreamReader, DataCollector 等

23. Interface 命名以 PascalCasing 方式,通常在最前面加上 I ,名字應以名詞為主,如 IList, IReadOnlyCollection 等

24. Variable 與 parameter 命名以 camelCasing 方式,名字應以名詞為主,如:
public int ToInt32(string itemValue, int itemCount);

25. Method 命名以 PascalCasing 方式,名字應以動詞為主,如:
ToString(), Write(), ExecuteCommand();

26. Property 命名以 PascalCasing 方式,名字應以名詞為主,如:
public int Length { get; set; }

27. Event 命名以 PascalCasing 方式,名字應以動詞為主,如:
public event EventHandler ObjectChanged;

28. Public field 命名以 PascalCasing 方式,而 private field 以 camelCasing 方式,名字應以名詞為主,如:
public TimeSpan Timeout;
private TimeSpan _firstTimeout;

29. Enum 命名以 PascalCasing 方式,名字應以名詞為主,如:
FileMode
{
Open,
Create,
Append,
}

30. Class 的名字通常和對應的 Interface 名字有關係,如:


31. 大部份的情況下, Enum 最好要加上 None 元素用來代表該 enum 用不到的情況下,並且將它設定為 0.如:


32. Enum 若是 Flags 的屬性,別在名字加上 Flag,可用複數名詞來代表. 如


33. 為了便於閱讀和 code review,將關聯性高的 class 成員寫在相鄰的位置.

34. 一個檔案最好只有一個 class,並且檔案名字與 class 名字一致.若是 nested class 或是一些簡單且臨時在程式間用的 class 除外.

35. 所有的宣告都應加上 public, private, internal.若是只在一個 method 裡使用的臨時變數可以忽略.

36. 在 switch 裡要加上 default: ,即使它的內容是空的,也要加上.

37. 要產生準確的 exception.如,當你檢查參數是否為空時,就該用 ArgumentNullException 而不是用 ArgumentException.

38. 產生 exception 時,要產生有意義且可知道錯誤細節與解決方法的 exception,不該把許多可能會發生錯誤的 code 放在一個 try block 裡,然後只有一個 catch (Exception ex).

39. 當處理商業邏輯時,很可能找不到適合的 exception type,此時應自行建立自訂的 exception.

40. Exception 產生時不應該影響程式的正常流程,並且 exception 產生時需被處理,例如 log.

41. 在 try-catch 裡若使用到一些資源,可在 finally block 裡釋放.如:


42. 在你的程式裡,在最上層 (可能是在 UI 層) 要能抓住所有可能會發生的 exception,做好相對應的處理或畫面顯示,避免程式中斷.

43. 在 catch block 裡應該都為該 exception 做些處理動作,而不是空白 (什麼事都不做).除非,程式裡已有邏輯在處理那段錯誤,如:


44. 不用重覆拋出 exception.應該加一些錯誤處理機制並且只需要 throw 即可.如:


45. 對於有實做 IDisposable 的 class 應善用 using 來確保資源能適時適當地被釋放.如:


別在 try-finally 裡使用 Dispose,如:


46. 盡可能使用 generic collection 來儲存物件,例如使用 List<T> 而不用 ArrayList.

47. 所有的 public class, property, method 都應該要有 XML 文件註解,如:


48. 有適合的語法糖時就使用,以便於閱讀.如:


49. 在不影響程式邏輯的情況下,應把程式碼編排縮排量減少.如:


50. Public method 的參數應盡量使用 interface,如:


51. 若無特別需求,為每個非同步呼叫加上 ConfigureAwait(false)

52. 若無特別需求,不自行建立 Thead ,一律使用 Task 讓 .NET threadpool 來管理相關資源.

53. 多個 Task 有執行順序時,善用 ContinueWith(), WhenAll(), 或 WhenAny(),而不該用 Task.Delay.

Share:

#36 Code Review - 檢視你的程式碼

不論是寫好一個新的功能或是修改 bug,在程式碼都能正確執行並且在 check-in 到 source control server 之前,一定都會有一個 code review 的動作.這件事情通常是由比較資深的人員或對整體程式較為熟悉的人員來執行.Code review 帶來的好處很多,在這裡不一一描述,這對比較資淺的人員或新進的人員來說都是一件好事.因為透過 code review,新進或資淺的人員可以比較快進入情況.有時我們進入了一個龐大程式碼的專案,短時間之久是蠻難完全了解所有的細節以及那個團隊的寫程式碼的文化,所以透過 code review 來了解程式,也算是一種學習.

如果你工作的地方有執行 code reivew 的動作,那真的恭喜你,畢竟教學相長,透過檢視彼此的程式碼是一種良好的學習方式.也因為這跟團隊文化與團隊技術能力與要求上有很大的關係,所以 code review 實在很難說有什麼標準可言.除了程式碼要符合團隊的寫作規定外,要求嚴格的團隊甚至會到極為嚴苛的地步.

接下來,我分享一些我以前在 code review 中學習到的經驗.

1. 每個公司或團隊因為人的不同,所以要求的標準也不同,先不用假設對方是來找你麻煩,如果他對你寫的程式有意見,他一定能說的出原因讓你相信他說的是對的.

2. 如果你寫的是像 Java/C# 之類的物件導向的程式,這樣可能就會有點小複雜.因為同一個功能因每個人對物件的設計會不同,再加上會導入一些 pattern,所以當你不明白全部的內容時,會很容易就不是故意了犯了一些錯.也不用過於放在心上,這種事情是常常發生的.

3. 以我個人而言,程式碼寫的簡單乾淨好閱讀就行了,但是你很可能會遇到資深的工程師要求你要改的更簡潔.例如,

bool visited;
if (isActive == true) {
    visited = true
}

改成

bool visited;
visited |= isActive

基本上這兩個邏輯是一樣的,如果你被要求要寫成下面那樣的語法,就當做是個學習經驗吧!
其他像是變數名稱,物件名稱或屬性名稱也是會有類似的情況.

4. Comment 寫註解.做產品的公司會連註解也有嚴格的格式要求.如果是一般的程式註解,也是要寫到讓人看的懂,否則有寫跟沒寫是一樣的.

所以,如果你是新人,那就好好享受 code review 的過程吧,因為當你遇到一位功力深厚的前輩幫你 code review 時,保證你會收益良多的.

Share:

#31 程式該怎麼分辨好壞呢 ?

上個星期看到一篇短文,文章網址如下
http://buzzorange.com/techorange/2015/10/08/the-six-most-common-species-of-code/

這文章很有趣,它列出不同的人會如何寫出同一個 function.

看到學生寫的就是很標準的學生該有的答案.儘管 recursive 的寫法會有 call stack overflow 的問題,但對一個學生來說,重點是練習 recursive 的思考,所以這樣寫蠻好的.

看到由 Hackathon 寫出來的答案,哈哈,老實說,我真的是打從心裡笑了出來.這個笑可不是嘲笑的笑,而是一種打從心裡佩服的笑,尤其是看到那一句註解 // good enough for the demo

再來看看新創公司寫出來的,其實看不出來這和學生寫的有什麼大差別.也許作者在暗示些什麼?

再來看看大公司.還真的是有大公司寫法的樣子.你是否曾想過大公司的寫法為何是這樣子呢 ? 明明是個很簡單的數學式子而己.我能想到的幾個原因如下:

1.  大公司所生產的產品都具有一定的規模,所以在具有規模的產品下,一定會有許多設計是為了滿足軟體設計的一致性.所以,可想而知,這種具有規模的程式碼一定都是抽象再抽象化,把物件導向常用到的觀念一定都會套用進去,所以你才會看到也許明明是一個簡單的動作卻要搞的好像很複雜的樣子.

2. 大公司也是從小公司漸漸演變上來的,程式經過長時間的演變並且很可能經過許多不同工程師的改進與維護,所以,一般人很難能很快速地看懂程式碼,因為實在有太多故事在裡面了.

3. 在大公司工作的工程師們,平均來說也都是書念的比較好,程式寫的比較好的人.這些人心中或許有一些優越感存在.這些人也許為了顯出自己的優秀,真的會寫出很優秀的架構與運作流程,但由於實在太優秀了,所以對一般人來說就比較不容易懂.

最後,我們再回到 Fibonacci 數列.在電腦與數學的世界裡,這是一個非常有名的數列.在學校學習有關 recursive 或程式設計時的基本練習題目.也許你也可能在面試的時候會遇到這一個題目.一般來說,大家都會直覺地用 recursive 來寫這個題目,因為學校課本是這樣練習的.以時間複雜度的角度來看,recursive 的寫法並不是最好的,更何況它會有 call stack overflow 的問題.我會建議大家改成用 dynamic programming 的寫法,從小的數字算起,一直累積到大的數字,時間複雜度只有 O(n),而要付出的代價就是較多的記憶體空間.所以,Fibonacci 若把 recursive 改成用 dynamic programming 來寫的話就是典型的用空間換時間的方法.

再回到主題,那程式到底要怎麼寫才好呢 ? 其實,我欣賞 Hackathon 的寫法,重點並不在於寫的好不好,重點是這樣的寫法非常能表達出來作者明白所需要達成的目的是什麼.所以,把問題回歸本質,我們應該要問的是,輸入參數會是什麼,需要輸出什麼,有多少的 CPU 與記憶體可以用.把目的搞懂,再把限制條件搞清楚,這樣寫出來的程式不會離好程式太遠了.我們只要在我們關心的情況下讓我們的程式能正常運作就行了.在限制條件或需求之外,程式會有問題的話,那也不是我們需要關心的.

Share:

#29 測試,該如何開始 ?

對一個好品質的軟體而言,測試的確是件很重要的事情.你寫的程式是否能在你假設的情況下做出正確的反應,這也是許多軟體工程師們的挑戰.一般較年輕的軟體工程師們往往過於著重在寫程式的技巧,所以常常忽略了花時間在學習如何測試你的程式.其實,軟體測試的學問完全不亞於一般的軟體開發所需的知識,而很多人可能會有一個先入為主的想法,那就是程式如何都寫不出來了,那學如何測試有什麼用呢 ? 聽起來好像還有幾分道理,至少在十多年前我是這樣想的.後來接觸多了之後也漸漸發現,如何不知道如何測試的話,那你怎麼能知道你寫出來的程式一定能用呢 ?

一般來說 (至少在我的工作環境下是如此),軟體工程師自己寫出來的 function 一定要自己寫好 unit test.這是蠻重要的事情,因為除了你,應該沒有人比你更了解你寫的 function 是什麼,但也因為如此,也容易造成自己在測試時會陷入一些假設的情境下而忘了一些測試條件.

我們來看一個很簡單的例子,

int Add(int x, int y) {
    return x+y;
}

以上是一個很簡單的加法,每個人都會寫.如果我們要為這個加法 function 寫一些 unit test,你會寫那些東西呢 ?

首先,我們先來找一些可行的例子,先來試試 test for pass,例如輸入 x=0, y=0 或輸入 x= 10, y = 10 之類的,如果你能得到正確的答案,那看來這個 function 是可以用的.

接下來,我們就要再多想一想,我需要把所有可能的數字組合都測試過一遍以確保這個 function 是正常的嗎 ? 如果你有時間的話,當然應該是要這麼做,因為一旦這個 function 送到客戶那執行時,你已經把所有可能的輸入組合都測試過了,所以你當然知道會不會有問題.但我們真的需要這麼做嗎 ? 其實也不用.關於這點,你可以在一般的軟體測試文章或書藉裡看到一個所謂邊界值條件的測試.據經驗來看,讓程式發生問題時有較高的機率會發生在邊界值的情況,例如 x = int.MaxValue 或 y=int.MaxValue,當你一輸入進去時,你就會發生這個 function 其實是有問題的,因為 integer overflow 了.

接下來,既然 x 和 y 是 int,那表示負數也是接收的,所以你還可以測試負數的邊界值,x = int.MinValue , y = int.MinValue,同樣的也會發生 overflow 的錯誤.所以,你就可以看到只要你把 x, y 的數值在可接受的範圍上跑了一遍,你就會發現這個加法 function 到底有沒有問題了.

接下來,你可能會說現在市面上寫的專案程式不是那麼單純呀.沒錯,我同意你的看法,的確都不單純,但寫 unit test 的精神還是一樣的.再看一個簡單的例子,

Result[] GetData( Connection conn, Command cmd) {
  .....
}

以上的 function 是一個根據命令 (Command) 在連線 (Connection) 上做一個取得資料的動作,取得到的資料將會以 Result[] array 的形式傳出來.

按照上述的第一步,你應該要先測試  test for pass 的情況,傳入正確的 connection 以及正確的 command,然後檢查是不是會得到結果.

接下來,我們可以調整 conn 和 cmd 的物件屬性,來測試 GetData 會如何反應.例如,故意把 cmd 輸入誤會的命令,或是故意把 conn 的狀態設定為關閉.甚至你還可以傳入空物件,看看 GetData 的行為是不是符合預期.

所以,你應該可以感覺到撰寫這些 unit test 的時間與力氣並不會比較少.尤其是一旦軟體能做的事情越多時,這些輸入參數的可能變化將是成指數形式往上成長,我們根本很難把所有可能的情況都試過一遍.如果你真的都試過了,那麼客戶會遇到的問題一定都會在你的掌握之中,甚至你可以在交貨之前就先修正這些問題了.但畢竟這只是理想,對一個具有某程規模以上的軟體而言,這幾乎是不太可能的事情,一來是時間,二來是預算等等的考量.所以,我們要做的也是在時間與預算範圍內能包含大部份的情況,比如 90% 的客戶都不會有問題等.

其實在設計程式時也有些技巧可以幫助你做測試,這些將都是未來文章的內容.


Share:

#20 The Power of Ten - 撰寫可靠軟體的思維

標題為 The Power of Ten - Rules for Developing Safety Critical Code 的文章刊登在 IEEE Computer 2006 年 6 月的月刊中,作者是位在 NASA JPL 實驗室的研究科學家,同時也是位學者和工程師.這篇文章可以在 http://spinroot.com/gerard/pdf/P10.pdf 看到.

若以武功修練來比喻的話,這篇文章就像是一個得道高人所寫下來的內功心法,他把自己在 JPL 實驗室工作的多年 C 語言工作心得濃縮成十項重點,作者也為每一個重點留下說明.雖然這一篇文章是將近十年前的文章了,但它的參考價值極高,而且我相信許多資深的軟體工程師幾乎都會同意作者所寫的內容.

其中有幾點並非在所有的程式語言中都會碰的到,尤其是跟記憶體管理有關的工作.以現在一般商業應用裡最常見的 C# 和 Java 語言都應該有其 runtime 環境的設計,所以記憶體管理的工作並不在程式設計者上,而是在開發其語言的公司上,所以這樣等於你把記憶體管理的工作交給其他人來處理了.我想這對 NASA 來說應該是不太能接受的事情,畢竟能打上太空的物體都是極為龐大的資金,因此 NASA 應該是不可能用類似像 C#, JAVA 這般的語言來打造火箭或衛星要用的軟體.回歸最基本的本質,用 C 應該是相當原始且基礎了.所以,在那篇文章裡,作者談了一些與 C 語言有關的準則.在這裡我不談特定語言,我們來看看其他的通用的準則.

1. Restrict all code to very simple control flow constructs – do not use goto statements, setjmp or longjmp constructs, and direct or indirect recursion.

這一點應用也沒什麼好再做說明的了,就是這樣,沒有 GOTO,沒有 recursion.所以在前面的文章裡,有些程式的寫法我提供了 recursive 和 iterative 的寫法,那時也提過 iterative 的寫法是比較好的.

2. All loops must have a fixed upper-bound. It must be trivially possible for a checking tool to prove statically that a preset upper-bound on the number of iterations of a loop cannot be exceeded. If the loop-bound cannot be proven statically, the rule is considered violated.

你想想看你寫程式時做到了這一點嗎? 你有沒有做過 while (true) 之類的語法呢 ?

4. No function should be longer than what can be printed on a single sheet of paper in a standard reference format with one line per statement and one line per declaration. Typically, this means no more than about 60 lines of code per function.

這是一定要的.若以 OO 的角度來看,有一個 function 能寫的很長,我都會懷疑程式碼一定在某些地方會有重覆功能的現象.

5. The assertion density of the code should average to a minimum of two assertions per function.

每個 function 平均來說至少有兩個檢查.這邊的檢查就是要防止一些不尋常的參數發生.比如你今天要寫一個加法,接收兩個 int ,如果傳入的參數是負數,那你的程式能處理嗎? 或是加起來的數字 overflow 了,你的程式能處理嗎?

6.  Data objects must be declared at the smallest possible level of scope

這應該是很直覺的想法,因為你不需要把不相干的資料物件提供給不需要該資料物件的程式碼.

7.  The return value of non-void functions must be checked by each calling function, and the validity of parameters must be checked inside each function.

這應該是每一個程式工程師要做的,只要呼叫的參數皆符合要求,那麼回傳值也需要檢查,同時還要注意是否有可能會產生其他的 exception ,都要記得做好 try catch 的反應動作.

10. All code must be compiled, from the first day of development, with all compiler warnings enabled at the compiler’s most pedantic setting.

最後這一點就是在考驗你們的專案裡有沒有良好的軟體工程流程以及 build system.工程師們一旦把程式碼 check-in 之後,所有的程式就應該要重新編譯,然後所有的測試案例都要執行.這樣才能即早發現問題,即早解決.


Share:

#17 客戶說程式碼不能有 recursive function,該怎麼辦 ?

不知大家有沒有被客戶這樣要求過,寫程式一律不準出現 recursive 的寫法.這也是十年前左右的故事了,並不是我個人遇到的狀況,是我朋友們遇過的情況.當時對這樣的要求沒太多的頭緒,不過心裡還是把這問題放著,也許有一天我會知道為什麼.

後來,有一次因為一個考試,突然讓我想到這個問題可能的答案,從技術角度來看的話,我想這應該是客戶的顧慮.情況是這樣的,在作業系統中,每個工作被執行的單位是 Process.一個作業系統裡會有許多的 Process 在執行,分別負責不同的功能,例如,有 Process 是專門負責處理 DNS 封包,有 Process 專門負責與印表機的通訊.所以,我們若自己寫一個程式在作業系統裡執行,這個程式也將是一個 Process,作業系統也就會負責他所需要做的工作內容.

在每個 Process 裡都會有一個獨立的記憶體空間用來儲存該 Process 需要的變數與資訊,其中有一項資訊叫 call stack,簡單的說是你的程式某個 function 呼叫了某個 function,然後再呼叫其他的 function,一直呼叫下去到作業系統的核心.因為這個記憶體空間的大小是有一個固定的量,所以當呼叫 function 一直無窮盡的進去下去的話,就很容易造成這個記憶體空間不夠用了,而形成了 call stack overflow 的情況.一旦這情況發生了,該 Process 就會被迫中止.

如果以作業系統的角度來想,程式裡不包含 recursive 寫法是很重要的.所以,一般的習慣都會採用 iterative 的寫法來取代 recursive 的寫法.在前面的 Binary search 文章裡,我們有介紹到 Binary search 的 recursive 寫法與 iterative 寫法.不同的情況會用到不同的寫法技巧,以 Binary search 來說,就是用兩個額外的 index 來做,如果是 Tree traversal 的話,通常是用 Stack 和 List 來把 recursive 寫法改成 iterative 寫法.

未來遇到適當的內容時,再來說明不同的 recusrive 寫法是如何改成 iterative 寫法.

Share: