前言
在使用 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<s