|
在《關于最近面試的一點感想》一文中,Michael同學談到他在面試時詢問對方“delegate在.NET framework1.1,2.0,3.5各可以怎么寫”這個問題。于是乎,有朋友回復道“請問樓主,茴香豆的茴有幾種寫法”,“當代孔乙己”,獨樂,眾樂。看了所有的評論,除了某些朋友認為“的確不該不知道這個問題”之外,似乎沒有什么人在明確支持樓主。
不過我支持,為什么?因為我也提過出這樣的問題。
這樣,我們暫且不提應聘“高級開發人員”的人,在“自稱熟悉各版本.NET框架”的前提下,是否應該知道這個答案。我們也暫且不提Michael同學提問的“目的”是什么。老趙就先單獨針對這個問題進行解釋,然后談談自己為什么會提出這個問題吧。
可能有一件事情需要說在前面,那就是:委托本身其實從來沒有改變過,改變的一直都是委托的“寫法”。因此更確切地說,改變的只是“編譯器”。而本文所有內容都用C#來實現,其實談得也都是C#編譯器本身——但是其實VB.NET也有變化啊。再由于.NET版本和C#版本的關系也是非常密切的,因此全文就使用.NET版本進行指代了。
.NET 1.x中委托的寫法
委托,如果不追究細節,從表面上來看我們可以將其通俗地理解為一個安全的“函數指針”。當然,這個函數指針其實也是一個對象,有自己的成員,也會封裝了被調用方的上下文等等。至于委托的定義和使用方式,則是這樣的:
public delegate int SomeDelegate(string arg1, bool arg2);public static int SomeMethod(string arg1, bool arg2) { return 0; }public class SomeClass{ public int SomeMethod(string a1, bool a2) { return 0; } public event SomeDelegate SomeEvent;}static void Main(string[] args){ SomeClass someClass = new SomeClass(); SomeDelegate someDelegate = new SomeDelegate(someClass.SomeMethod); someClass.SomeEvent += new SomeDelegate(SomeMethod);}
可見,在.NET 1.x中需要使用new DelegateType(...)的方式來創建一個委托對象。不過,作為委托對象內部的方法它既可以是實例方法,也可以是靜態方法。此外,方法只需要匹配委托類型的簽名和返回值即可,方法參數的名稱不會成為約束。
嗯,就是這么簡單。
.NET 2.0中委托的寫法
.NET中的委托引入了范型,且寫法略有簡化:
public delegate TResult MyFunc<T1, T2, TResult>(T1 a1, T2 a2);public static int SomeMethod(string a1, bool a2) { return 0; }static void Main(string[] args){ MyFunc<string, bool, int> myFunc = SomeMethod;}
在.NET 2.0中,new DelegateType已經可以省略,開發人員可以直接將方法賦值給一個委托對象的引用。當然,這個改進不值一提,.NET 2.0中委托寫法的關鍵在于引入了“匿名方法”:
public static void TestRequest(string url){ WebRequest request = HttpWebRequest.Create(url); request.BeginGetResponse(delegate(IAsyncResult ar) { using (WebResponse response = request.EndGetResponse(ar)) { Console.WriteLine("{0}: {1}", url, response.ContentLength); } }, null);}
匿名方法,簡單地說就是內聯在方法內部的委托對象,它的關鍵便在于形成了一個閉包(委托執行時所需的上下文)。如上面的代碼中,BeginGetResponse的第一個參數(委托)可以直接使用TestRequest方法的參數url,以及方法內的“局部”變量request。如果沒有匿名函數這個特性的話,代碼寫起來就麻煩了,例如在.NET 1.x中您可能就必須這么寫:
展開
折疊public static void TestRequest(string url){ WebRequest request = HttpWebRequest.Create(url); object[] context = new object[] { url, request }; request.BeginGetResponse(TestAsyncCallback, context);}public static void TestAsyncCallback(IAsyncResult ar){ object[] context = (object[])ar.AsyncState; string url = (string)context[0]; WebRequest request = (WebRequest)context[1]; using (WebResponse response = request.EndGetResponse(ar)) { Console.WriteLine("{0}: {1}", url, response.ContentLength); }}
此時,我們往往會發現,開發人員需要花費大量的精力,為一小部分代碼維護一大段上下文。例如在這段代碼中,我們會將url和request對象塞入一個object數組中,在回調函數中再通過危險的Cast操作恢復數據。如果您希望“強類型”,那么只能為每個回調創建一個新的上下文對象,維護起來可能更加麻煩——要知道,在并行編程,異步調用越來越重要的今天,如果沒有匿名方法自動保留上下文的特性,開發人員會為這些“額外工作”疲于奔命的。
可能您會說,匿名方法的可讀性不佳,因為需要“內聯”。一個方法中內聯太多,維護成本就上去了,所以匿名方法并不推薦使用。我想說的是,您錯了。如果為了可維護性,要將方法獨立拆開,也可以利用匿名方法的優勢:
public static void TestRequest(string url){ WebRequest request = HttpWebRequest.Create(url); request.BeginGetResponse(delegate(IAsyncResult ar) { TestAsyncCallback(ar, request, url); }, null);}public static void TestAsyncCallback(IAsyncResult ar, WebRequest request, string url){ using (WebResponse response = request.EndGetResponse(ar)) { Console.WriteLine("{0}: {1}", url, response.ContentLength); }}
如果借助.NET 3.5中的Lambda表達式,代碼可以寫的更簡單易讀:
public static void TestRequest(string url){ WebRequest request = HttpWebRequest.Create(url); request.BeginGetResponse(ar => TestAsyncCallback(ar, request, url), null);}
匿名方法的作用
千萬不要小看匿名方法的作用,有些時候您認為它的作用僅限于上文描述,只是因為沒有在某些問題上踏前一步。例如,對于那些只需要“按需創建”,且要“線程安全”的對象,您會怎么做呢?沒錯,可以使用Double Check:
private object m_mutex = new object();private bool m_initialized = false;private BigInstance m_instance = null;public BigInstance Instance{ get { if (!this.m_initialized) { lock (this.m_mutex) { if (!this.m_initialized) { this.m_instance = new BigInstance(); this.m_initialized = true; } } } return this.m_instance; }}
嗯,做的很漂亮!那么……這樣的屬性再來一個,再來三個,再來五個呢?可能有些朋友就會開始大段地Copy & Paste,于是錯誤便難免了。這里有一件真人真事,以前某位同學在一堆這樣的代碼中迷茫了,說為什么用了這種方法,還是初始化了多次對象了?檢查了半天沒有看出問題來。最后發現,原因是訪問了錯誤的initialized變量(例如,在某個應該訪問artistInitialized的地方訪問了articleInitialized)。可惜,大段時間已經被浪費了——更糟的是,心情也隨之變差了。
其實,Copy & Paste很明顯沒有遵守DRY原則啊。為什么不把它們封裝在一處呢?例如:
展開
折疊public class Lazy<T>{ public Lazy(Func<T> func) { this.m_initialized = false; this.m_func = func; this.m_mutex = new object(); } private Func<T> m_func; private bool m_initialized; private object m_mutex; private T m_value; public T Value { get { if (!this.m_initialized) { lock (this.m_mutex) { if (!this.m_initialized) { this.m_value = this.m_func(); this.m_func = null; this.m_initialized = true; } } } return this.m_value; } }}
于是,之前的代碼就可以簡化成這樣了:
private Lazy<BigInstance> m_lazyInstance = new Lazy<BigInstance>(delegate { return new BigInstance(); });public BigInstance Instance { get { return this.m_lazyInstance.Value; } }
還是太丑,上Lambda表達式!
private Lazy<BigInstance> m_lazyInstance = new Lazy<BigInstance>(() => new BigInstance());public BigInstance Instance { get { return this.m_lazyInstance.Value; } }
如果沒有匿名方法,許多容易使用的編程模型和方式都難以開展。例如,我們就不會有CacheHelper,也不會有AsyncTaskDispatcher(上,下),也很難利用“延遲”所帶來的便利,更難以出現微軟并行擴展、CCR等優秀框架。可以這么說,如果您不善于使用委托,您如果不知道如何合適地使用匿名方法,您在不自知的情況下可能就已經編寫了大量額外的代碼了。
老趙平時的工作之一,便是為項目提供各種擴展API,可以讓程序員們更愉快地進行開發工作,得到更好的生產力,讓代碼變得更加美好。如今C#有了匿名方法、Lambda表達式、表達式樹、擴展方法等優秀的語言特性,真讓我有“如魚得水”的感覺。因此,我對于Java這樣不思進取的語言可以說深惡痛絕(Java朋友們趕快學習Scala吧)。在看閱讀大量Java開源項目代碼時,我常有這樣的感覺:“如果是C#的話,利用匿名方法,這個類不就可以不寫,那個類就可以省略……”。沒錯,為了保留回調函數的上下文而創建一些類,對于C#程序員來說,的確是一件有些不可思議的事情。
至于Lambda表達式以及其他話題,我們下次再說吧。
匿名方法的缺點
匿名方法的優勢在于自動形成閉包,而它的缺點也是讓程序員“不自覺”地創建了閉包,這會讓某些對象的生命周期加長。例如在一開始的TestRequest方法中,表面上看起來url是參數,request是局部變量,有些朋友可能會認為它們在方法退出后就已經準備回收了。不過因為形成了閉包,url和request已經“升級”為一個對象的域變量,它的生命周期延長了,延長至回調函數執行完畢。因此,一不注意可能就會產生一些莫名其妙的情況。
其實,這些都是“延遲”所帶來的陷阱,作為一個優秀的開發人員,除了知道某個東西的作用和優勢,也要知道它的問題,不是嗎?
總結
您現在還覺得.NET中的“委托”是一個簡單的,只適合“初學者”,一搜就都知道的概念嗎?
您可能會說“是”,不過對我來說這真不簡單,我也是慢慢才意識到這些的。雖然關于委托的大量內容可以在互聯網上搜索而來,但還是有太多東西是需要在大量編程實踐中積累下來的——等一下,這不就是“高級開發人員”和“初學者”的主要區別之一嗎?
嘲笑孔乙己的朋友們,你們在一味鄙視“茴”的四種寫法的同時,說不定也失去了一個了解中國傳統文化的機會呢!
剩下的下次再說吧,Lambda表達式還等著我們。
相關文章
NET技術:從.NET中委托寫法的演變談開去(上):委托與匿名方法,轉載需保留來源!
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯系我們修改或刪除,多謝。