---------- 20180926 Tuple ref: https://dotblogs.com.tw/hatelove/archive/2013/12/12/tuple-introduction.aspx 有時候在開發時,總會碰到一種需求:希望回傳不只一個值。 想要傳入多個值比較簡單一些,因為方法參數沒有個數限制,但一般的 function 只能回傳一個值,如果要多個值,通常有幾種方式: 1. 自訂一個 class 或 struct ,來代表這幾個值是內聚在一起描述某種相同的職責所需的特徵。 2. 透過物件身上的 filed/member 來存放經過這個方法之後,物件狀態的轉變,需要時再取用物件狀態即可。 3. 透過 out 或 byref 的宣告,把其他要回傳的多個值,在呼叫時先傳進來。 然而透過 class 或 struct 可能會碰到幾個問題: 1. 回傳的多個值,有時候並不帶有強烈內聚的關係。被放在 class 或是 struct 並不妥當,只是單純為了放在一起而新增了一個容器。 2. 這些 class 或 struct 有可能只被這個方法使用,為了這樣的需求而新增一個無法重用且無法明確表達同一職責的 class 或 struct, 容易撐爆跟污染你的產品。 透過物件的狀態來取用多個值,則是一般常見的物件導向設計方式。但仍有可能碰到: 1. 當方法只是單純的 static 方法時,方法內只能取用 static 的 filed/member/property ,可能碰到 thread-safe 問題,同一時間點多個 thread 同時取用,會被互相影響。 2. 期望方法回傳值的生命週期僅限於這個方法內,而非整個物件內都可以存取,以避免互相影響或被其他方法改變。 用 out 或 byref 是一般常見的作法,就像 .NET Framework 中的 TryXXX() 方法,回傳一個 bool, 但轉型或要取得處理完成的結果,要透過 out 的方式,把期望的結果變數當參數傳進去。問題: 1. out 跟 byref 的用法很囉嗦,例如要先在方法外面宣告,或是在方法裡面一定要重新 assign 。 2. 當需要多個值時,方法參數個數會因為回傳值而越來越多。一堆 out 或 byref 的參數,顯得相當礙眼。 因此,這篇文章要介紹一下 .NET Framework 4.0 的 Tuple 來滿足上述的需求。 Tuple 就是拿來做拋棄式 DTO 或拋棄式的 struct/class 用途。如Action 與 Func 也是匿名委派的一種形式。 就是把烙烙長的 delegate TResult Func(T1 p1) 變成 Func 而已,搭配 Lambda 來讓 Action 與 Func 的方法內容寫得更輕鬆。所謂的匿名,幾乎就等同於拋棄式的意思。可能只在這邊用一次,其他地方不需要使用,或是它並不是什麼需要被內聚在某個介面或類別底下的職責。 例如 void TestTuple() { string sMsg = string.Empty; int iAge = 150; // 回傳 Boolean 以及 out 參數. if (!Validate(iAge, out sMsg)) Console.WriteLine(sMsg); // 回傳一個 Tuple值, 內含item1, item2 欄位. var rTuple = ValidateTuple(iAge); if (!rTuple.Item1) Console.WriteLine(rTuple.Item2); } // 回傳 Boolean 以及 out 參數. Boolean Validate(int iAge, out string sMsg) { sMsg = string.Empty; if (iAge > 100) { sMsg = "invalid age."; return false; } return true; } // 回傳一個 Tuple值, 內含item1, item2 欄位. Tuple ValidateTuple(int iAge) { if (iAge > 100) { return Tuple.Create(false, "invalid age from tuple."); } return Tuple.Create(true, string.Empty); } 想要回傳2個結果,就是使用 Tuple ,要建立一個 Tuple 也相當簡單,只需要透過 Tuple 的 static funciton: Create() 把想要回傳的結果傳入,就會選用對應的多載方法,回傳對應的 Tuple 泛型型別。 呼叫端要拿第一個結果,就只要取用 Item1 這個 property,要拿第二個結果,就只需要取用 Item2 這個 property, 依此類推。 雖然 property 的名字使用 ItemN 無法顯現對應的結果意義為何,但這是一個非 dynamic 且強型別的 type ,使用起來還是相當簡便。 要建立一個 Tuple ,建議直接使用 Tuple.Create() ,Create() 會依據傳入的參數個數,以及對應的型別進行型別推論,比用 Tuple 的 constructor 來得簡便得多。 瞭解了 Tuple 的使用方式,其實就可以直接想像這個 class 是怎麼定義的,例如: public class MyTuple { public T1 Item1 { get; set; } public T2 Item2 { get; set; } } 也因為這樣的用途,所以在選用 Tuple 時,建議不要用太多泛型,否則 Item1~Item7,呼叫端要能分辨這些代表什麼,是困難的。也因此 Tuple 的定義,分成兩塊,一個是T1~T7,至少可以支援 7 個 Item 。 如果需要第 8 個以上,則要透過 Tuple 類別,第 8 個TRest 代表其他的 Item, Item8 的型別也是 Tuple ,透過這種方式來達成無限多層 Tuple 的結構。如 MSDN 上的範例: var from1980 = Tuple.Create(1203339, 1027974, 951270); var from1910 = new Tuple> (465766, 993078, 1568622, 1623452, 1849568, 1670144, 1511462, from1980); var population = new Tuple>> ("Detroit", 1860, 45619, 79577, 116340, 205876, 285704, from1910); by the way, 這時候你就會覺得 var 的出現很美妙了。 不然又是落落長的宣告... 結論與建議 其實各位讀者可以發現,Item1, Item2, Item3 就像 Column[0], Column[1] 的感覺,很簡便使用,但卻也很容易造成可讀性下降的問題。因此,要使用 Tuple, 建議團隊中要制訂一定的規範。我自己是習慣: 1. Tuple 的 Item 不超過 3 個。 2. Function 的 summary 註解中要說明各個 Item 代表的意義。 3. 不要跨太多層 call stack 使用。建議不要超過 2 層,也就是呼叫完拿到之後,就不要再直接往外丟。 4. 另一個適合用的地方是 private function 簡便性與可讀性的取捨,一定要做好,否則程式碼中充斥著 Item1, Item8.Item1 這樣的東西,過兩個禮拜回來看的時候,我想連作者都會忘記 Item8.Item3 代表的意思是什麼。