中庸
C#是靜態類型語言,要使用類型必須引用該類型的定義。因此,從軟件組織角度會發生組件間的引用依賴關系。常見的引用依賴關系有兩種模式:
a. 正向依賴:組件A用到了組件B中定義的類T,組件A直接引用組件B,依賴關系是“組件A -> 組件B”。
b. 反向依賴:組件A通過接口I定義功能規范,針對抽象編程;組件B反過來引用組件A,並定義類T實現接口I;由另一組件C將I與T粘合起來,依賴關系是“組件A <- 組件B”。這就是著名的IoC方式。
簡單說來,IoC是“誰制定規范,誰就擁有控制權;誰執行規范,誰就被控制”。如果規范借助於C#的靜態類型檢查,比如接口或抽象類,那麼規范就表現出較強的語法約束性,使得組件A的編寫比較獨立,而組件B則受制與組件A。
本系列的第一篇舉了一個基於接口的IoC例子,我們看到當需要采用第三方組件時,為了適用接口的靜態類型約束,不得不增加一個adapter去實現接口並包裝對第三方組件的調用。這表現出基於接口的IoC在粘合規范與實現時不太靈活。
但是,規范和類型約束沒有必然的聯系。在基於委托的IoC例子中,我們不需要任何的adapter,就能輕松的粘合規范與實現,表現出較強的靈活性。這就是通過委托定義規范,不會造成組件B對組件A的依賴,組件A和組件B的實現都顯得比較獨立。
實際上,我們還可以有比委托更靈活的規范表達方式,比如:通過HTTP + XML來表達規范,這樣甚至是語言無關的,完全可能組件A由C#編寫,組件B由Java編寫。
上面列舉的3種規范定義方式:基於接口、基於委托、基於HTTP + XML分別代表了由約束到協議,由嚴格到靈活的3種風格。當然,還有更多的方式,但這裡只列舉這三種作為代表。動與靜之間需要把握一個分寸,接口過於死板;而HTTP + XML的方式則完全是基於運行時協議的,需要自己做很多檢查工作;委托的好處在於既消除了組件A、B的依賴關系,又能享受IDE智能提示和編譯器檢查(簽名檢查)等好處。因此,委托是把動與靜結合得恰到好處的中庸之道。
Duck Typing
但可惜委托還無法覆蓋接口或類的所有功能,有朋友提到“接口是對象功能的抽象,而委托是方法功能的抽象”就是這個意思。那麼我們自然會想,有沒有一種方式,能將委托的思想應用於對象呢?有!它就是:duck typing。前文已經談到,duck typing關注“對象能做什麼”或者說“如何使用對象”,對象繼承自什麼類,或者實現什麼接口並不重要。duck typing的本意為“如果一只動物,走起來像鴨子,叫起來像鴨子,我就可以把它當作鴨子”。與繼承性多態相對應,duck typing可以實現非繼承多態。按duck typing的本意,那麼更純正的duck typing看起來應該是這個樣子:
static void Main(string[] args)
{
object person= new Person();
IPerson duck= Duck.Create<IPerson>(person);//創建鴨子對象
Console.WriteLine(duck.Name + " will be " + (duck.Age + 1) + "next year");
duck.Play("basketball");
Console.WriteLine(duck.Mother);//為null
//duck無法調用duck.Sing()
}
interface IPerson
{
string Name { get; }
int Age { get; }
string Mother { get; }
void Play(string ball);
}
class Person
{
public string Name { get { return "Todd"; } }
public int Age { get { return 26; } }
public void Play(string ball) { Console.WriteLine("Play " + ball); }
public void Sing(string song) { Console.WriteLine("Sing " + song");}
}
上面的例子中,雖然person對象沒有實現IPerson接口,我們一樣可以通過Duck.Create<IPerson>(person)創建鴨子對象調用person的屬性和方法。這種把接口和對象粘合的方式與委托和方法的粘合方式非常接近,真正達到了我們所謂把委托思想應用於對象的想法。
C#中要實現Duck.Create<T>的功能,可以通過Emit動態創建實現T接口的代理類,在代理類中攔截方法調用,並將方法調用轉換成target對象上的反射調用。Castle開源項目的DynamicProxy是一個很好用的工具,在它的幫助下很容易實現代理類的創建和方法調用的攔截。
動態類型
事實上,duck typing是動態類型概念的一種。C#4.0已經通過dynamic關鍵字來實現動態類型,讓我們先來看看下面的示例:
string json = @"{ ""FirstName"": ""John"", ""LastName"": ""Smith"", ""Age"": 21 }";
dynamic person = CreateFromJson(json);
Console.WriteLine("{0} will be {1} next year", person.FirstName, person.Age + 1);
Console.WriteLine(person.ToJson());
person.Play("basketball");//不存在的方法,可以通過編譯,但會拋出運行時異常
通過dynamic關鍵字,我們不需要在編譯時為person對象指定類型,編譯器不會進行類型檢查,而是將對象的屬性訪問和方法調用轉換為反射調用,所以,只要對象的運行時類型能通過反射找到匹配的屬性或方法即可。
上面的例子通過json創建了一個dynamic對象,就像javascript中操作json一樣方便。在運行 時,person.FirstName和person.Age能通過反射正確地進行屬性訪問,person.ToJson()也可以正確地執行,但 person.Play( "basketball")由於運行時類型不存在該方法而拋出異常。
C#4.0的味道如何?很爽嗎?不過,說實在的,我覺得有點兒不太舒服了!仔細想想,它像接口,像委托,還是更像HTTP + XML? 對於dynamic對象,編譯器不進行對象類型檢查,不進行屬性類型檢查,也不進行方法簽名檢查。很明顯,它像HTTP+XML,完全基於運行時協議,沒有一點兒靜態的東西。如果類比委托的話,更理想的方式應該是,不進行對象類型檢查,但進行屬性類型和方法簽名檢查,就像下面這樣:
string json = @"{ ""FirstName"": ""John"", ""LastName"": ""Smith"", ""Age"": 21 }";
dynamic person = CreateFromJson(json);
Console.WriteLine("{0} will be {1} next year", person.FirstName<string>, person.Age<int> + 1);
Console.WriteLine(person.ToJson<string>());
person.Play<string>("basketball");
string firstName = person.FirstName<string>;
int age = person.Age<int>;
Func<string> toJson = person.ToJson<Func<string>>;
Action<string> play = person.Play<Action<string>>;
這樣,除了屬性和方法的名稱是動態的外,屬性的類型和方法的簽名都是靜態的,把運行時錯誤的可能降到最低,同時享受靜態檢查的好處。其實,沿著這個思路,我們大可不必等著C#4.0的dynamic才開始動態類型,在C#2.0時代也可以這樣:
object jsonObj = CreateFromJson(@"{ ""FirstName"": ""John"", ""LastName"": ""Smith"", ""Age"": 21 }");
Dynamic person = new Dynamic(jsonObject);
string firstName = person.Property<string>("FirstName");
int age = person.Age<int>("Age");
Func<string> toJson = person.Method<Func<string>>("ToJson");
Action<string> play = person.Method<Action<string>>("Play");
看到這裡,相信您一定明白該如何實現Dynamic類了吧?如果覺得有用,就自己嘗試實現一下吧!
後續
下一篇打算繼續探討在C#模擬實現動態類型,敬請關注!