前言
在使用 Lambda 表達式時,我們常會碰到一些典型的應用場景,而從常用場景中抽取出來的應用方式可以描述為應用模式。這些模式可能不全是新的模式,有的參考自 JavaScript 的設計模式,但至少我看到了一些人為它們打上了名字標簽。無論名字的好與壞,我還是決定給這些模式進行命名,至少這些名字很具有描述性。同時我也會給出這些模式的可用性、強大的部分和危險的部分。提前先說明:絕大多數模式是非常強大的,但有可能在代碼中引入些潛在的 Bug。所以,慎用。
目錄導航
- 回調模式 (Callback Pattern)
- 函數作為返回值 (Returning Functions)
- 自定義函數 (Self-Defining Functions)
- 立即調用的函數表達式 (Immediately-Invoked Function Expression)
- 對象即時初始化 (Immediate Object Initialization)
- 初始化時間分支(Init-Time Branching)
- 延遲加載 (Lazy Loading)
- 屬性多態模式 (Lambda Property Polymorphism Pattern)
- 函數字典模式 (Function Dictionary Pattern)
- 函數式特性 (Functional Attribute Pattern)
- 避免循環引用 (Avoiding cyclic references)
回調模式 (Callback Pattern)
老生常談了。事實上,在 .NET 的第一個版本中就已經支持回調模式了,但形式有所不同。現在通過 Lambda 表達式中的閉包和局部變量捕獲,這個功能變得越來越有趣了。現在我們的代碼可以類似于:
1 void CreateTextBox() 2 { 3 var tb = new TextBox(); 4 tb.IsReadOnly = true; 5 tb.Text = "Please wait ..."; 6 DoSomeStuff(() => 7 { 8 tb.Text = string.Empty; 9 tb.IsReadOnly = false; 10 }); 11 } 12 13 void DoSomeStuff(Action callback) 14 { 15 // Do some stuff - asynchronous would be helpful ... 16 callback(); 17 }
對于 JavaScript 開發人員,這個模式已經沒什么新鮮的了。而且通常我們在大量的使用這種模式,因為其非常的有用。例如我們可以使用時間處理器作為參數來處理 AJAX 相關的事件等。在 LINQ 中,我們也使用了這個模式,例如 LINQ 中的 Where 會在每次迭代中回調查詢函數。這些僅是些能夠說明回調模式非常有用的簡單的示例。在 .NET 中,通常推薦使用事件回調機制。原因有兩點,一是已經提供了特殊的關鍵字和類型模式(有兩個參數,一個是發送者,一個數事件參數,而發送者通常是 object 類型,而事件參數通常從 EventArgs 繼承),同時通過使用 += 和 -+ 操作符,也提供了調用多個方法的機會。
函數作為返回值 (Returning Functions)
就像常見的函數一樣,Lambda 表達式可以返回一個函數指針(委托實例)。這就意味著我們能夠使用一個 Lambda 表達式來創建并返回另一個 Lambda 表達式。這種行為在很多場景下都是非常有用的。我們先來看下面這個例子:
1 Func<string, string> SayMyName(string language) 2 { 3 switch (language.ToLower()) 4 { 5 case "fr": 6 return name => 7 { 8 return "Je m'appelle " + name + "."; 9 }; 10 case "de": 11 return name => 12 { 13 return "Mein Name ist " + name + "."; 14 }; 15 default: 16 return name => 17 { 18 return "My name is " + name + "."; 19 }; 20 } 21 } 22 23 void Main() 24 { 25 var lang = "de"; 26 //Get language - e.g. by current OS settings 27 var smn = SayMyName(lang); 28 var name = Console.ReadLine(); 29 var sentence = smn(name); 30 Console.WriteLine(sentence); 31 }
這段代碼可以寫的更簡潔些。如果請求的語言類型未找到,我們可以直接拋出一個異常,以此來避免返回一個默認值。當然,出于演示的目的,這個例子展示了類似于一種函數工廠。另外一種方式是引入 Hashtable ,或者更好的 Dictionary<K, V> 類型。
1 static class Translations 2 { 3 static readonly Dictionary<string, Func<string, string>> smnFunctions 4 = new Dictionary<string, Func<string, string>>(); 5 6 static Translations() 7 { 8 smnFunctions.Add("fr", name => "Je m'appelle " + name + "."); 9 smnFunctions.Add("de", name => "Mein Name ist " + name + "."); 10 smnFunctions.Add("en", name => "My name is " + name + "."); 11 } 12 13 public static Func<string, string> GetSayMyName(string language) 14 { 15 //Check if the language is available has been omitted on purpose 16 return smnFunctions[language]; 17 } 18 } 19 20 // Now it is sufficient to call Translations.GetSayMyName("de") 21 // to get the function with the German translation.
盡管這看起來有點過度設計之嫌,但畢竟這種方式很容易擴展,并且可以應用到很多場景下。如果結合反射一起使用,可以使程序變得更靈活,易于維護,并且更健壯。下面展示了這個模式如何工作:
自定義函數 (Self-Defining Functions)
在 JavaScript 中,自定義函數是一種極其常見的設計技巧,并且在某些代碼中可以獲得更好的性能。這個模式的主要思想就是,將一個函數作為一個屬性,而此屬性可以被其他函數很容易的更改。
1 class SomeClass 2 { 3 public Func<int> NextPrime 4 { 5 get; 6 private set; 7 } 8 9 int prime; 10 11 public SomeClass() 12 { 13 NextPrime = () => 14 { 15 prime = 2; 16 17 NextPrime = () => 18 { 19 //Algorithm to determine next - starting at prime 20 //Set prime 21 return prime; 22 }; 23 24 return prime; 25 }; 26 } 27 }
在這里做了什么呢?首先我們得到了第一個質數,值為 2。這不是重點,重點在于我們可以調整算法來排除所有偶數。這在一定程度上會加快我們的算法,但我們仍然設置 2 為質數的起點。我們無需看到是否已經調用了 NextPrime() 函數,因為根據函數內的定義會直接返回 2。通過這種方式,我們節省了資源,并且能夠優化算法。
同樣,我們也看到了這么做可以性能會更好。讓我們來看下下面這個例子:
1 Action<int> loopBody = i => { 2 if(i == 1000) 3 loopBody = /* set to the body for the rest of the operations */; 4 5 /* body for the first 1000 iterations */ 6 }; 7 8 for(int j = 0; j < 10000000; j++) 9 loopBody(j);
這里我們有兩個截然不同的區域,一個是前 1000 次迭代,另一個是剩下的 9999000 次迭代。通常我們需要一個條件來區分這兩種情況。大部分情況下會引起不必要的開銷,這也就是我們為什么要使用自定義函數在執行一小段代碼后來改變其自身。
立即調用的函數表達式 (Immediately-Invoked Function Expression)
在 JavaScript 中,立即調用函數表達式(簡寫為 IIFE)是非常常見的用法。原因是 JavaScript 并沒有使用類似 C# 中的大括號方式來組織變量的作用域,而是根據函數塊來劃分的。因此變量會污染全局對象,通常是 window 對象,這并不是我們期待的效果。
解決辦法也很簡單,盡管大括號并沒有給定作用域,但函數給定了,因為在函數體內定義的變量的作用域均被限制在函數內部。而 JavaScript 的使用者通常認為如果那些函數只是為了直接執行,為其中的變量和語句指定名稱然后再執行就變成了一種浪費。還有一個原因就是這些函數僅需要執行一次。
在 C# 中,我們可以簡單編寫如下的函數達到同樣的功能。在這里我們同樣也得到了一個全新的作用域,但這并不是我們的主要目的,因為如果需要的話,我們可以在任何地方創建新的作用域。
1 (() => { 2 // Do Something here! 3 })();
代碼看起來很簡單。如果我們需要傳遞一些參數,則需要指定參數的類型。
1 ((string s, int no) => { 2 // Do Something here! 3 })("Example", 8);
看起來寫了這么多行代碼并沒有給我們帶來什么好處。盡管如此,我們可以將這個模式和 async 關鍵字結合使用。
1 await (async (string s, int no) => { 2 // Do Something here async using Tasks! 3 })("Example", 8); 4 5 //Continue here after the task has been finished
這樣,類似于異步包裝器的用法就形成了。
對象即時初始化 (Immediate Object Initialization)
將這個模式包含在這篇文章當中的原因是,匿名對象這個功能太強大了,而且其不僅能包含簡單的類型,而且還能包含 Lambda 表達式。
1 //Create anonymous object 2 var person = new 3 { 4 Name = "Florian", 5 Age = 28, 6 Ask = (string question) => 7 { 8 Console.WriteLine("The answer to `" + question + "` is certainly 42!"); 9 } 10 }; 11 12 //Execute function 13 person.Ask("Why are you doing this?");
如果你運行了上面這段代碼,可能你會看到一個異常(至少我看到了)。原因是,Lambda 表達式不能被直接賦予匿名對象。如果你覺得不可思議,那我們的感覺就一樣了。幸運的是,編譯器告訴了我們“老兄,我不知道我應該為這個 Lambda 表達式創建什么樣的委托類型”。既然這樣,我們就幫下編譯器。
1 var person = new 2 { 3 Name = "Florian", 4 Age = 28, 5 Ask = (Action<string>)((string question) => 6 { 7 Console.WriteLine("The answer to `" + question + "` is certainly 42!"); 8 }) 9 };
一個問題就出現了:這里的函數(Ask 方法)的作用域是什么?答案是,它就存活在創建這個匿名對象的類中,或者如果它使用了被捕獲變量則存在于其自己的作用域中。所以,編譯器仍然創建了一個匿名對象,然后將指向所創建的 Lambda 表達式的委托對象賦值給屬性 Ask。
注意:當我們想在匿名對象中直接設定的 Lambda 表達式中訪問匿名對象的任一屬性時,則盡量避免使用這個模式。原因是:C# 編譯器要求每個對象在被使用前需要先被聲明。在這種情況下,使用肯定在聲明之后,但是編譯器是怎么知道的?從編譯器的角度來看,在這種情況下聲明與使用是同時發生的,因此變量 person 還沒有被聲明。
有一個辦法可以幫助我們解決這個問題(實際上辦法有很多,但依我的觀點,這種方式是最優雅的)。
1 dynamic person = null; 2 person = new 3 { 4 Name = "Florian", 5 Age = 28, 6 Ask = (Action<string>)((string question) => 7 { 8 Console.WriteLine("The answer to `" + question + "` is certainly 42! My age is " + person.Age + "."); 9 }) 10 }; 11 12 //Execute function 13 person.Ask("Why are you doing this?");
看,現在我們先聲明了它。當然我們也可以直接將 person 聲明為 object 類型,但通過這種方式我們可以使用反射來訪問匿名對象中的屬性。此處我們依托于 DLR (Dynamic Language Runtime)來實現,這應該是最好的包裝方式了。現在,這代碼看起來很有 JavaScript 范兒了,但實際上我不知道這東西到底有什么用。
初始化時間分支(Init-Time Branching)
這個模式與自定義函數模式密切相關。唯一的不同就是,函數不再定義其自身,而是通過其他函數定義。當然,其他函數也可能沒有通過傳統的方式去定義,而是通過覆蓋屬性。
這個模式通常也稱為加載時分支(Load-Time Branching),本質上是一種優化模式。該模式被用于避免恒定的 switch-case 和 if-else 等控制結構的使用。所以在某種程度上可以說,這種模式為某些恒定代碼分支之間建立了聯系。
1 public Action AutoSave { get; private set; } 2 3 public void ReadSettings(Settings settings) 4 { 5 /* Read some settings of the user */ 6 7 if (settings.EnableAutoSave) 8 AutoSave = () => { /* Perform Auto Save */ }; 9 else 10 AutoSave = () => { }; //Just do nothing! 11 }
這里我們做了兩件事。首先,我們有一個方法讀取了用戶設置信息。如果我們發現用于已經打開了自動保存功能,則我們將保存代碼賦予該屬性。否則我們僅是指定一個空方法。然后,我們就可以一直調用 AutoSave 屬性在執行操作。而且在此之后我們不再需要檢查用戶設置信息了。我們也不需要將這個特定的設置保存到一個 boolean 變量中,因為響應的函數已經被動態的設定了。
你可能說這并沒有太大的性能改善,但這只是一個簡單的例子。在一些復雜的代碼中,這種方法確實可以節省很多時間,尤其是在大循環中調用那個動態設置的方法時。
同時,這樣的代碼可能更易于維護,并非常易讀。在省去了很多不必要的控制過程之后,我們能夠直達重點:調用 AutoSave 函數。
在 JavaScript 中,這種模式常用于檢測瀏覽器的功能集。瀏覽器功能的檢測對于任何網站來說都是噩夢一樣,而這個模式在實現中就顯得非常有用。同樣 jQuery 也使用了同樣的模式來檢測正確的對象,以便使用 AJAX 功能。一旦它識別出瀏覽器支持 XMLHttpRequest ,則因為瀏覽器不會在腳本執行期間變化,所以無需在考慮處理 ActiveX 對象了。
延遲加載 (Lazy Loading)
我們想要創建一個對象,它能夠執行某種延遲加載操作。也就是說,盡管對象已經被正確地初始化了,但我們并沒有加載所有需要的資源。一個原因是想避免在獲取需要的數據時引發的大量的 IO 操作。同時,我們也想在準備使用數據時,數據盡可能是最新的。有多種方式可以實現這個功能,而在 Entity Framework 中使用了效率極高的 LINQ 來解決延遲加載的情況。其中,IQueryable<T> 僅存儲了查詢而沒有存儲基礎的數據。一旦我們需要這些數據,不僅已構造的查詢會被執行,而且查詢也是以最高效的形式來執行,例如在遠端數據庫服務器上執行 SQL 查詢語句。
在我們想要的場景中,我們需要區別兩種狀況。首先我們進行查詢,然后后續的操作將在已經獲取到的結果上進行。
1 class LazyLoad 2 { 3 public LazyLoad() 4 { 5 Search = query => 6 { 7 var source = Database.SearchQuery(query); 8 9 Search = subquery => 10 { 11 var filtered = source.Filter(subquery); 12 13 foreach (var result in filtered) 14 yield return result; 15 }; 16 17 foreach (var result in source) 18 yield return result; 19 }; 20 } 21 22 public Func<string, IEnumerable<ResultObject>> Search { get; private set; } 23 }
那么,在這里基本上我們需要設置兩個不同的方法。一個是從數據庫拉數據,另一個是從已獲取到的數據中進行過濾。當然你可能會想我們也可以在類中創建另一個方法來設置這些行為或者使用其他方式可能更有效。
屬性多態模式 (Lambda Property Polymorphism Pattern)
Lambda表達式可以被用于實現多態(override),而不需要使用 abstract 和 virtual 等關鍵字。
1 class MyBaseClass 2 { 3 public Action SomeAction { get; protected set; } 4 5 public MyBaseClass() 6 { 7 SomeAction = () => 8 { 9 //Do something! 10 }; 11 } 12 }
這里沒什么特別的。我們創建了一個類,通過一個屬性來暴露一個函數。這有點像 JavaScript 風格。有趣的地方不僅在于可以在這個類中控制和更改這個函數屬性,而且可以在它的衍生類中更改。
1 class MyInheritedClass : MyBaseClass 2 { 3 public MyInheritedClass 4 { 5 SomeAction = () => { 6 //Do something different! 7 }; 8 } 9 }
看!實際上這里我們能夠更改這個屬性完全是依賴于 protected 的應用。這種方式的缺點是我們無法直接訪問父類的實現。這里我們丟失了 base 的強大能力,因為 base 中的屬性具有相同的值。如果你確實還需要這樣的功能,我建議使用下面這種“模式”:
1 class MyBaseClass 2 { 3 public Action SomeAction { get; private set; } 4 5 Stack<Action> previousActions; 6 7 protected void AddSomeAction(Action newMethod) 8 { 9 previousActions.Push(SomeAction); 10 SomeAction = newMethod; 11 } 12 13 protected void RemoveSomeAction() 14 { 15 if (previousActions.Count == 0) 16 return; 17 18 SomeAction = previousActions.Pop(); 19 } 20 21 public MyBaseClass() 22 { 23 previousActions = new Stack<Action>(); 24 25 SomeAction = () => 26 { 27 //Do something! 28 }; 29 } 30 }
這樣,在子類中只能調用 AddSomeAction() 來覆寫當前已設置的方法。這個方法將被直接放入一個棧內,這使我們能夠記錄之前的狀態。
我給這個模式起的名字是 Lambda屬性多態模式(Lambda Property Polymorphism Pattern)。它主要描述將函數封裝為屬性的可能性,然后能夠在衍生類中覆寫父類的屬性。上面代碼中的棧只是一個額外的功能,并不會改變這個模式的目標。
為什么需要這個模式?坦白的說,有多種原因。首先就是因為我們能這么做。但要注意,實際上如果我們要使用多個不同的屬性時,這個模式會變得更靈活。“多態”這個詞也就有了全新的含義,但那就是另一個模式了。所以這里我主要是想強調這個模式可以實現一些以前曾認為不可能的功能。
例如:你想覆寫一個靜態方法(不推薦這么做,但或許這么做是能解決你的問題的最優雅的方法)。那么,繼承是不可能改變靜態方法的。原因很簡單:繼承僅應用于類的實例,而靜態方法卻沒有被綁定到類的實例上。靜態方法對所有的類的實例都是相同的。這里也蘊含著一個警告,下面的這個模式可能不沒有達到你想要的結果,所以一定要明確你為什么要這么用。
1 void Main() 2 { 3 var mother = HotDaughter.Activator().Message; 4 //mother = "I am the mother" 5 var create = new HotDaughter(); 6 var daughter = HotDaughter.Activator().Message; 7 //daughter = "I am the daughter" 8 } 9 10 class CoolMother 11 { 12 public static Func<CoolMother> Activator { get; protected set; } 13 14 //We are only doing this to avoid NULL references! 15 static CoolMother() 16 { 17 Activator = () => new CoolMother(); 18 } 19 20 public CoolMother() 21 { 22 //Message of every mother 23 Message = "I am the mother"; 24 } 25 26 public string Message { get; protected set; } 27 } 28 29 class HotDaughter : CoolMother 30 { 31 public HotDaughter() 32 { 33 //Once this constructor has been "touched" we set the Activator ... 34 Activator = () => new HotDaughter(); 35 //Message of every daughter 36 Message = "I am the daughter"; 37 } 38 }
這是一個極其簡單的示例,并且希望不要引起誤導。如果這么用可能會導致事情變的更復雜,所以我一直說為什么我們需要避免這么用,只是描述了其可行性。關于靜態多態的較好的方案總不是易于實現的,并且需要很多的代碼,所以除非它真能幫你解決實際的問題,而不是讓你更頭痛。
函數字典模式 (Function Dictionary Pattern)
之前我已經介紹了這個模式,只是還沒有指定名字,它就是函數字典模式(Function Dictionary Pattern)。這個模式的基本成分包括:一個哈希表或字典用于包含一些鍵值對,鍵可能是任意類型,值是某些類型的函數。這個模式也指定了一個特殊的字典構造方式。這在這個模式中是必須的,否則只能使用 switch-case 來達到相同的目的了。
1 public Action GetFinalizer(string input) 2 { 3 switch 4 { 5 case "random": 6 return () => { /* ... */ }; 7 case "dynamic": 8 return () => { /* ... */ }; 9 default: 10 return () => { /* ... */ }; 11 } 12 }
上面代碼中我們需要一個字典類型嗎?當然。我們可以這么做:
1 Dictionary<string, Action> finalizers; 2 3 public void BuildFinalizers() 4 { 5 finalizers = new Dictionary<string, Action>(); 6 finalizers.Add("random", () => { /* ... */ }); 7 finalizers.Add("dynamic", () => { /* ... */ }); 8 } 9 10 public Action GetFinalizer(string input) 11 { 12 if(finalizers.ContainsKey(input)) 13 return finalizers[input]; 14 15 return () => { /* ... */ }; 16 }
但要注意,在這里使用這個模式并沒有帶來任何好處。實際上,這個模式的效率更低,并且需要更多格外的代碼。但是我們能做的事情是,通過反射來是函數字典的構造過程自動化。同樣還是沒有使用 switch-case 語句的效率高,但代碼更健壯,可維護性更高。實際上這個操作也很方便,比如我們有大量的代碼,我們甚至不知道在哪個方法內加入 switch-case 代碼塊。
我們來看一個可能的實現。通常我會建議在代碼中增加一些約定,以便能夠得到字典的鍵。當然,我們也可以通過選擇類中某個屬性的名稱,或者直接使用方法的名稱來滿足需求。在下面的示例中,我們僅選擇一種約定:
1 static Dictionary<string, Action> finalizers; 2 3 //The method should be called by a static constructor or something similar 4 //The only requirement is that we built 5 public static void BuildFinalizers() 6 { 7 finalizers = new Dictionary<string, Action>(); 8 9 //Get all types of the current (= where the code is contained) assembly 10 var types = Assembly.GetExecutingAssembly().GetTypes(); 11 12 foreach (var type in types) 13 { 14 //We check if the class is of a certain type 15 if (type.IsSubclassOf(typeof(MyMotherClass))) 16 { 17 //Get the constructor 18 var m = type.GetConstructor(Type.EmptyTypes); 19 20 //If there is an empty constructor invoke it 21 if (m != null) 22 { 23 var instance = m.Invoke(null) as MyMotherClass; 24 //Apply the convention to get the name - in this case just we pretend it is as simple as 25 var name = type.Name.Remove("Mother"); 26 //Name could be different, but let's just pretend the method is named MyMethod 27 var method = instance.MyMethod; 28 29 finalizers.Add(name, method); 30 } 31 } 32 } 33 } 34 35 public Action GetFinalizer(string input) 36 { 37 if (finalizers.ContainsKey(input)) 38 return finalizers[input]; 39 40 return () => { /* ... */ }; 41 }
現在這段代碼是不是更好些呢。事實上,這個模式可以節省很多工作。而其中最好的就是:它允許你實現類似插件的模式,并且使此功能跨程序集應用。為什么這么說呢?比如我們可以掃描指定模式的類庫,并將其加入到代碼中。通過這種方式也可以將其他類庫中的功能添加到當前代碼中。
1 //The start is the same 2 3 internal static void BuildInitialFinalizers() 4 { 5 finalizers = new Dictionary<string, Action>(); 6 LoadPlugin(Assembly.GetExecutingAssembly()); 7 } 8 9 public static void LoadPlugin(Assembly assembly) 10 { 11 //This line has changed 12 var types = assembly.GetTypes(); 13 14 //The rest is identical! Perfectly refactored and obtained a new useful method 15 foreach (var type in types) 16 { 17 if (type.IsSubclassOf(typeof(MyMotherClass))) 18 { 19 var m = type.GetConstructor(Type.EmptyTypes); 20 21 if (m != null) 22 { 23 var instance = m.Invoke(null) as MyMotherClass; 24 var name = type.Name.Remove("Mother"); 25 var method = instance.MyMethod; 26 finalizers.Add(name, method); 27 } 28 } 29 } 30 } 31 32 //The call is the same
現在我們僅需要通過一個點來指定插件。最后將會從某路徑中讀取類庫,嘗試創建程序集對象,然后調用 LoadPlugin() 來加載程序集。
函數式特性 (Functional Attribute Pattern)
Attribute 是 C# 語言中最棒的功能之一。借助 Attribute,曾在 C/C++ 中不太容易實現的功能,在C#中僅需少量的代碼即可實現。 這個模式將 Attribute 與 Lambda 表達式結合到一起。在最后,函數式特性模式(Functional Attribute Pattern)將會提高 Attribute 應用的可能性和生產力。
可以說,將 Lambda 表達式和 Attribute 結合到也一起相當的有幫助,因為我們不再需要編寫特定的類。讓我們來看個例子來具體解釋是什么意思。
1 class MyClass 2 { 3 public bool MyProperty 4 { 5 get; 6 set; 7 } 8 }
現在針對這個類的實例,我們想要能夠根據一些領域特性語言或腳本語言來改變這個屬性。然后我們還想能夠在不寫任何額外代碼的條件下來改變屬性的值。當然,我們還是需要一些反射機制。同時也需要一些 attribute 來指定是否這個屬性值能夠被用戶更改。
1 class MyClass 2 { 3 [NumberToBooleanConverter] 4 [StringToBooleanConverter] 5 public bool MyProperty 6 { 7 get; 8 set; 9 } 10 }
我們定義兩種轉換器。雖然使用一個即可標示這個屬性可以被任何用于更改。我們使用兩個來為使用者提供更多的可能性。在這個場景下,一個使用者可能實際上使用一個字符串來設置這個值(將字符串轉換成布爾值)或者用一個數字(比如0或1)。
那么這些轉換器如何實現呢?我們來看下 StringToBooleanConverterAttribute 的實現。
1 public class StringToBooleanConverterAttribute : ValueConverterAttribute 2 { 3 public StringToBooleanConverterAttribute() 4 : base(typeof(string), v => { 5 var str = (v as string ?? string.Empty).ToLower(); 6 7 if (str == "on") 8 return true; 9 else if (str == "off") 10 return false; 11 12 throw new Exception("The only valid input arguments are [ on, off ]. You entered " + str + "."); 13 }) 14 { 15 /* Nothing here on purpose */ 16 } 17 } 18 19 public abstract class ValueConverterAttribute : Attribute 20 { 21 public ValueConverterAttribute(Type expected, Func<object, object> converter) 22 { 23 Converter = converter; 24 Expected = expected; 25 } 26 27 public ValueConverterAttribute(Type expected) 28 { 29 Expected = expected; 30 } 31 32 public Func<Value, object> Converter { get; set; } 33 34 public object Convert(object argument) 35 { 36 return Converter.Invoke(argument); 37 } 38 39 public bool CanConvertFrom(object argument) 40 { 41 return Expected.IsInstanceOfType(argument); 42 } 43 44 public Type Expected 45 { 46 get; 47 set; 48 } 49 50 public string Type 51 { 52 get { return Expected.Name; } 53 } 54 }
使用這個模式我們得到了什么好處呢?如果 Attribute 能夠接受非常量表達式作為參數(比如委托、Lambda 表達式等都有可能),則我們得到的好處會更多。通過這種方式,我們僅需使用 Lambda 表達式來替換抽象方法,然后將其傳遞給父類的構造函數。
你可能有些意見,這和 abstract 函數比并沒什么新鮮的,但有趣的地方在于不能像使用函數一樣來用,而是作為一個屬性能夠被外部進行設置。這可以被用于一些動態代碼中,來重寫一些轉換器,盡管其已經被實例化了。
避免循環引用 (Avoiding cyclic references)
在 C#中,循環引用并不是一個大問題。實際上僅在一種方式下會使循環引用帶來問題,那就是在 struct 結構體中。因為類是引用類型,循環引用并沒有什么壞處。在源對象上持有目標對象的一個引用指針,而在目標對象上持有一個源對象的引用指針,這不會有任何問題。
但是如果是結構體,我們沒法使用指針,其在棧上創建對象。因為在這種情況下,若源對象包含一個目標對象,實際上是包含了一個目標對象的拷貝,而不是真正的目標對象,而反過來也一樣。
大部分情況下,編譯器會檢測到這種循環引用,然后拋出一個編譯錯誤,這個功能其實很棒。我們來看個能引起錯誤的例子:
1 struct FirstStruct 2 { 3 public SecondStruct Target; 4 } 5 6 struct SecondStruct 7 { 8 public FirstStruct Source; 9 }
這上面的代碼中,使用結構體變量。這與類有巨大的不同:盡管我們沒初始化變量,但變量其實已經被初始化為默認值。
所以說,編程是件復雜的事,編譯器也不是萬能的神。通過一些方式可以騙過編譯器。如果我們欺騙了編譯器,編譯器就會告訴我們一個運行時錯誤,無法創建這個對象。一種欺騙方式是使用自動屬性:
1 struct FirstStruct 2 { 3 public SecondStruct Target { get; set; } 4 } 5 6 struct SecondStruct 7 { 8 public FirstStruct Source { get; set; } 9 }
這不會阻止問題的發生,其只是將問題從編譯時錯誤延遲到了運行時錯誤。我們腦中立即會產生一個方案,就是使用可空結構(nullable struct)。
1 struct FirstStruct 2 { 3 public SecondStruct? Target { get; set; } 4 } 5 6 struct SecondStruct 7 { 8 public FirstStruct Source { get; set; } 9 }
這里的問題是,那些可空結構也同樣是結構體,他們繼承自 System.Nullable<T> ,實際上也是一個結構體類型。
終于,Lambda表達式來拯救我們了。
1 struct FirstStruct 2 { 3 readonly Func<SecondStruct> f; 4 5 public FirstStruct(SecondStruct target) 6 { 7 f = () => target; 8 } 9 10 public SecondStruct Target 11 { 12 get 13 { 14 return f(); 15 } 16 } 17 } 18 19 struct SecondStruct 20 { 21 public FirstStruct Source { get; set; } 22 }
這里我們做了什么呢?我們使用了一個對函數的引用,而該函數會返回給我們結構體。編譯器會生成一個類來包含這個結構體,這樣這個結構體就作為一個全局變量存在了。因為結構體總是會包含一個默認的構造函數,會保持 f 的未引用狀態,我們加了另一個構造函數,并且將目標結構體作為參數傳入。
最后,我們創建了一個閉包,在其中返回被捕獲的結構體實例。重點的強調下,可能會有其他可能性。如果使用一個引用類型作為值類型的容器,可能循環引用的情況更糟。Lambda 表達式只是能完成這個功能的一種方式,但在某些條件下,其是能處理這種場景的最具表達性和最直接的方式。
完整代碼

1 class TestPatterns 2 { 3 public static void SelfDefining() 4 { 5 Console.WriteLine(":: Pattern: Self-definining function"); 6 7 Action foo = () => 8 { 9 Console.WriteLine("Hi there!"); 10 11 foo = () => 12 { 13 Console.WriteLine("Hi again!"); 14 }; 15 }; 16 17 Console.WriteLine("First call (initilization)."); 18 foo(); 19 Console.WriteLine("Second call - use different one now!"); 20 foo(); 21 Console.WriteLine("Third call - still the same."); 22 foo(); 23 } 24 25 public static void Callback() 26 { 27 Console.WriteLine(":: Pattern: Callback pattern"); 28 Console.WriteLine("Calling the function with lambda expression."); 29 30 CallMe(() => "The boss."); 31 32 Console.WriteLine("Back at the starting point."); 33 } 34 35 static void CallMe(Func<string> caller) 36 { 37 Console.WriteLine("Received function as parameter - Who called?!"); 38 Console.WriteLine(caller()); 39 } 40 41 public static void Returning() 42 { 43 Console.WriteLine(":: Pattern: Returning function"); 44 Console.WriteLine("Calling to obtain the method ..."); 45 Func<double, double> method = GetProperMethod("sin"); 46 Console.WriteLine("Doing something with the method ..."); 47 Console.WriteLine("f(pi / 4) = {0}", method(Math.PI / 4)); 48 } 49 50 static Func<double, double> GetProperMethod(string what) 51 { 52 switch (what) 53 { 54 case "sin": 55 return Math.Sin; 56 57 case "cos": 58 return Math.Cos; 59 60 case "exp": 61 return Math.Exp; 62 63 default: 64 return x => x; 65 } 66 } 67 68 public static void IIFE() 69 { 70 Console.WriteLine(":: Pattern: IIFE"); 71 72 ((Action<double>)((x) => 73 { 74 Console.WriteLine(2.0 * x * x - 0.5 * x); 75 }))(1.0); 76 77 78 ((Action<double, double>)((x, y) => 79 { 80 Console.WriteLine(2.0 * x * y - 1.5 * x); 81 }))(2.0, 3.0); 82 } 83 84 public static void ImmediateObject() 85 { 86 Console.WriteLine(":: Pattern: Immediate object initialization"); 87 88 var terminator = new 89 { 90 Typ = "T1000", 91 Health = 100, 92 Hit = (Func<double, double>)((x) => 93 { 94 return 100.0 * Math.Exp(-x); 95 }) 96 }; 97 98 Console.WriteLine("Terminator with type {0} has been created.", terminator.Typ); 99 Console.WriteLine("Let's hit the terminator with 0.5. Rest health would be {0}!", terminator.Hit(0.5)); 100 } 101 102 public static void InitTimeBranching() 103 { 104 Console.WriteLine(":: Pattern: Init-time branching"); 105 Action<int> loopBody = null; 106 Console.WriteLine("Select a proper loop body method ..."); 107 Random r = new Random(); 108 int sum = 0; 109 110 if (r.NextDouble() < 0.5) 111 { 112 Console.WriteLine("Selected random choice ..."); 113 114 loopBody = index => 115 { 116 sum += r.Next(0, 10000); 117 }; 118 } 119 else 120 { 121 Console.WriteLine("Selected little gauss ..."); 122 123 loopBody = index => 124 { 125 sum += index; 126 }; 127 } 128 129 Console.WriteLine("Execute the loop ..."); 130 131 for (var i = 0; i < 10000; i++) 132 loopBody(i); 133 134 Console.WriteLine("Loop has finished with result sum = {0}.", sum); 135 } 136 }
文章內容翻譯并改編自 Way to Lambda ,章節和代碼有很大的改動,未包含全部內容。
文章列表