Review後看到標題讓我十分羞愧自己語文功底太差,估計...請見諒......我還特地把這句寫回開頭了......
前天遇到的一個問題,所以在MSDN發了個問,剛也豐富了下問題,關於泛型的。
最近用EF嘗試DDD常常有些奇怪的想法,比如“EF的Model First能否添加泛型支持”。這次是“泛型的類型能否有帶參數的約束方式”。
具體想法很簡單,在我使用泛型的時候,我發現我需要實例化一個類型參數:
1 class MyClass<T> 2 { 3 public MyClass1() 4 { 5 this.MyObject = new T(); 6 } 7 8 T MyObject { get; set; } 9 }
當然,上面會報錯。
錯誤內容是T沒有一個new約束(new constraint),查閱下MSDN,得到了泛型的類型參數的new約束的內容。
所以接下來正確的代碼就是:
1 class MyClass<T> 2 where T : new() 3 { 4 public MyClass1() 5 { 6 this.MyObject = new T(); 7 } 8 9 T MyObject { get; set; } 10 }
然後,後來我發現,我需要根據參數來創建新的對象,而且該方法在泛型的構造函數中實現最合適,所以我希望有這樣的代碼:
1 class MyClass1<T> 2 where T : new(string) 3 { 4 public MyClass(string request) 5 { 6 this.MyObject = new T(request); 7 } 8 9 T MyObject { get; set; } 10 }
可惜這下就錯大了,然後查閱泛型的約束方式列表,發現根本沒有帶參數的構造函數這種約束。
所以就發生了我上面在MSDN上問的那個問題,尋求一個“優雅的解決方案”。
一般解決方案就像問題中的回答那樣有兩種,也是我試過但是很不爽的兩種,我們依次看看。
補充:
首先是Factory Pattern,就是建一個工廠類,先看看代碼,這是其中一種寫法,請不要糾結在Factory Pattern上:
1 class MyClass<T, TFactory> 2 where TFactory : IFactory<T>, new() 3 { 4 public MyClass(string request) 5 { 6 var factory = new TFactory(); 7 8 this.MyObject = factory.New(request); 9 } 10 11 T MyObject { get; set; } 12 } 13 14 interface IFactory<T> 15 { 16 T New(string request); 17 }
實現中你會發現,這樣需要為每個派生類或者實例類別創建並維護一個Factory類,那樣泛型本身就沒那麼大意義了,本來就是為了減少類型重用邏輯而采用泛型的。
如果不想維護多一個類,那麼就在目標類本身下手,所以我們可以為目標類創建一個基類:
1 class MyClass<T> 2 where T : TBase, new() 3 { 4 public MyClass(string request) 5 { 6 this.MyObject = T.New(request); 7 } 8 9 T MyObject { get; set; } 10 } 11 12 abstract class TBase 13 { 14 public abstract static TBase New(string request); 15 }
為了防止誤人子弟,首先要說在前頭的是,這樣寫是會編譯錯誤的!
約束上是沒錯的,但是它報的錯誤是類似於“T是個類型參數,不能這麼用!”('T' is a 'type parameter', which is not valid in the given context)。
還有一種基礎的做法反而忘記了,由James.Ying提醒想起來,就是從泛型類的構造函數傳入。
class MyClass<T> where T : TBase, new() { public MyClass(T myObject) { this.MyObject = myObject; } T MyObject { get; set; } }
這種方式使得泛型類簡潔多了,把實例化的過程交給了調用者,有點依賴倒置了(其實凡是應該在泛型裡實現的而交給了調用者或者繼承者都是這樣)。
優點是泛型簡單了,缺點就是你無法保證實例化使用的構造函數是T(string)。另外,它可能會降低代碼的重用性。假設實例化是有條件地,而且所有派生類的邏輯是統一的,那麼還是在泛型基類中實現比較好。
簡單情況下這是對泛型來說最優雅的方式了。
該方法可以在http://msdn.microsoft.com/en-us/library/system.activator.createinstance(v=vs.110).aspx見到,說明就比較明確了:
用最匹配的構造函數創建一個類型的實例(Creates an instance of the specified type using the constructor that best matches the specified parameters)。
寫法也很爽:
class MyClass<T> { public MyClass(string request) { this.MyObject = (T)Activator.CreateInstance(typeof(T), request); } T MyObject { get; set; } }
這種方法做得到,也很簡短,也不用多做接口和基類。
缺點就是沒有約束,沒辦法保證T能有帶指定數量和類型參數的構造函數,或者是否有構造函數。
如果T不符合設計需求的話會報相應的異常。
至此,便可以知道,C#的泛型裡,類型參數是一種“非類”的存在,類型參數的約束(Constraints on Type Parameters)僅僅是用來描述具體的類在實例化或者繼承時所需要達到的條件。而在泛型內部,類型參數僅僅是一種“特別的存在”,它用來描述類,但卻無法用作類。
首先,其實這個問題本身就是泛型的類型參數能否有帶參數的實例化方式,比如 T myObject = new T("Hello World!“) 。
然後,由於類型參數是用“約束”的方式來進行實例類的特點的描述的,所以,問題才變成了泛型的類型參數能否有帶參數的構造函數的約束方式,比如 where T : new(string) 。
要做假設的話,起始就是個證偽的問題,要證明它存在是否會造成什麼原則問題。
首先能對比的就是泛型的類型參數已經有了不帶參數的構造函數的約束方式了,那麼泛型的類型參數就算有帶了參數的構造函數的約束方式又如何?至少,泛型的類型參數已經有了不帶參數的構造函數的約束方式證明了泛型的類型參數有構造函數的約束方式並不會造成什麼問題而且技術上是可以實現的。(......)
在我們實例化一個新對象的時候通常會用兩種初始化方式:
大部分情況下兩種方式產生的結果是差不多的,這種大部分情況是指一般所涉及到的屬性或參數都是公開的(public),本來就是開放讀寫的,所以內部寫和外部寫都差不多。
但遇到一些情況,比如一些業務約束,需要對參數進行處理或者利用參數進行操作,最終操作結果是私密的(private),那麼就會偏向於選用構造函數傳參。或者會使用一個特殊的方法,由該方法在類實例化之後再把需要的數據帶進來進行操作,這麼做些許有失“一氣呵成”的爽快。
利用構造函數傳參並不是什麼容易替代的方式,因為它在絕大部分屬於它的場景裡都是最優的解決方案。有時候,初始化一個對象到使用,一氣呵成是最好的,因為這個事務本身就有很強的原子性。一個對象的兩種初始化方式造成了雙入口的麻煩,作為該類的使用者,有時候你會模糊,兩種方式所產生的結果你無法准確地把握;對於開發者,兩種實現方式供的出現在規范上也要求要麼二選一,要麼保證兩者一致。當類變得相對復雜的時候,事情就沒那麼簡單了。
所以,我們確實會需要泛型的類型參數有帶了參數的構造函數的約束方式的一些場景。它雖然不是必要的,但是絕對是一種需要,就像get/set訪問器那樣。
假設它可以由帶參數的構造函數約束了,那麼可不可以直接如約束那樣當作類來使用呢?比如調用靜態方法?在泛型中創建繼承於該類型參數的類?
如此種種算來發現每一種都有可能是特例,而不是一個簡單的實現即可解決的。
比如調用靜態方法來說,T.Hello()所涉及的就是執行的時候T能明確是哪個類;而在泛型類中創建繼承於該類型參數的類就會變得復雜。
單單想想調用那個類的方法:MyClass<T>.MySubClass,這裡的語法就有點“不一般”了,未必是一個“僅僅是泛型本身的問題”。
逼格高一點地說,越來越多的功能對C#或者任何一門語言來說是一條正確的道路嗎?
如果你有不滿,可以提供合適的標題,禁止以任何方式攻擊作者!