[C#]Tuple 簡介

  • 27958
  • 0
  • 2013-12-13

[C#]Tuple 簡介

前言

有時候在開發時,總會碰到一種需求:我希望回傳不只一個值。

想要傳入多個值比較簡單一些,因為方法參數沒有個數限制,但一般的 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 來滿足上述的需求。

 

What is Tuple

看 MSDN 的時候,Tuple 看起來頗嚇人,就像 Action, Func 一樣,有著一堆 generic 的定義。(Action<T1, T2> 與 Func<T1, TResult> 如果讀者還不熟悉,可以參考前面的文章:[.NET]快快樂樂學LINQ系列前哨戰-Func, Action, Predicate),如下圖所示:

image

image

其實 Action 與 Func 也是匿名委派的一種形式,說穿了,就是把烙烙長的 delegate TResult Func<T1>(T1 p1) 變成 Func<T1, TResult> 而已,搭配 Lambda 來讓 Action 與 Func 的方法內容寫得更輕鬆。所謂的匿名,幾乎就等同於拋棄式的意思。可能只在這邊用一次,其他地方不需要使用,或是它並不是什麼需要被內聚在某個介面或類別底下的職責。

用上述的角度來看 Tuple 就不難理解, Tuple 就是拿來做拋棄式 DTO 或拋棄式的 struct/class 用途。Tuple 常見的用途,可從 MSDN 上面的說明窺知一二,如下所示:

image

來看一下一個簡單的範例,程式碼如下:

image

如同前面的 TryParse() 一樣,往往在 Validate 的行為時,也需要得到驗證結果,包含是否驗證通過或不通過,以及驗證錯誤的相關訊息。這時如果使用 out 的作法,就會如上面例子一般,需要額外寫很多有的沒的。

同樣的目的,轉成 Tuple<T> 就會顯得簡潔多了,程式碼如下所示:

image

想要回傳2個結果,就是使用 Tuple<T1, T2> ,要建立一個 Tuple<T1, T2> 也相當簡單,只需要透過 Tuple 的 static funciton: Create() 把想要回傳的結果傳入,就會選用對應的多載方法,回傳對應的 Tuple 泛型型別。

呼叫端要拿第一個結果,就只要取用 Item1 這個 property,要拿第二個結果,就只需要取用 Item2 這個 property, 依此類推。

雖然 property 的名字使用 ItemN 無法顯現對應的結果意義為何,但這是一個非 dynamic 且強型別的 type ,使用起來還是相當簡便。

要建立一個 Tuple<T1, T2, ..., Tn> ,建議直接使用 Tuple.Create() ,Create() 會依據傳入的參數個數,以及對應的型別進行型別推論,比用 Tuple 的 constructor 來得簡便得多。

瞭解了 Tuple<T> 的使用方式,其實就可以直接想像這個 class 是怎麼定義的,例如:

    public class MyTuple<T1, T2>
    {
        public T1 Item1 { get; set; }
        public T2 Item2 { get; set; }
    }

也因為這樣的用途,所以在選用 Tuple 時,建議不要用太多泛型,否則 Item1~Item7,呼叫端要能分辨這些代表什麼,是困難的。也因此 Tuple<T> 的定義,分成兩塊,一個是T1~T7,至少可以支援 7 個 Item 。也就是下圖的部分:

image

如果需要第 8 個以上,則要透過 Tuple<T1, T2, T3, T4, T5, T6, T7, TRest> 類別,第 8 個TRest 代表其他的 Item, Item8 的型別也是 Tuple ,透過這種方式來達成無限多層 Tuple 的結構。如 MSDN 上的範例:

var from1980 = Tuple.Create(1203339, 1027974, 951270);
var from1910 = new Tuple<int, int, int, int, int, int, int, Tuple<int, int, int>> 
    (465766, 993078, 1568622, 1623452, 1849568, 1670144, 1511462, from1980);
var population = new Tuple<string, int, int, int, int, int, int,
    Tuple<int, int, int, int, int, int, int, Tuple<int, int, int>>> 
    ("Detroit", 1860, 45619, 79577, 116340, 205876, 285704, from1910);

by the way, 這時候你就會覺得 var 的出現很美妙了。

 

結論與建議

其實各位讀者可以發現,Item1, Item2, Item3 就像 Column[0], Column[1] 的感覺,很簡便使用,但卻也很容易造成可讀性下降的問題。因此,要使用 Tuple<T>, 建議團隊中要制訂一定的規範。我自己是習慣:

  1. Tuple 的 Item 不超過 3 個。
  2. Function 的 summary 註解中要說明各個 Item 代表的意義。
  3. 不要跨太多層 call stack 使用。建議不要超過 2 層,也就是呼叫完拿到之後,就不要再直接往外丟。
  4. 另一個適合用的地方是 private function

簡便性與可讀性的取捨,一定要做好,否則程式碼中充斥著 Item1, Item8.Item1 這樣的東西,過兩個禮拜回來看的時候,我想連作者都會忘記 Item8.Item3 代表的意思是什麼。


blog 與課程更新內容,請前往新站位置:http://tdd.best/