在.NET 1.0 的时候,大家都知道我们经常用到的是委托。有了委托呢,我们就可以像传递变量一样的传递方法。在一定程序上来讲,委托是一种强类型的托管的方法指针,曾经也一时被我们用的那叫一个广泛呀,但是总的来说委托使用起来还是有一些繁琐。来看看使用一个委托一共要以下几个步骤:
后来,幸运的是.NET 2.0 为了们带来了泛型。于是我们有了泛型类,泛型方法,更重要的是泛型委托。最终 在.NET3.5 的时候,我们 Microsoft 的兄弟们终于意识到其实我们只需要 2 个泛型委托(使用了重载)就可以覆盖 99%的使用场景了。
这样我们就可以跳过上面的第一步了,不过第 2 步还是必须的,只是用 Action 或者 Func 替换了。别忘了在.NET2.0 的时候我们还有匿名方法,虽然它没怎么流行起来,但是我们也给它 一个露脸的机会。
Func<double, double> square = delegate (double x) {
return x * x;
}
最后,终于轮到我们的 Lambda 优雅的登场了。
// 编译器不知道后面到底是什么玩意,所以我们这里不能用var关键字
Action dummyLambda = () => { Console.WriteLine("Hello World from a Lambda expression!"); };
// double y = square(25);
Func<double, double> square = x => x * x;
// double z = product(9, 5);
Func<double, double, double> product = (x, y) => x * y;
// printProduct(9, 5);
Action<double, double> printProduct = (x, y) => { Console.WriteLine(x * y); };
// var sum = dotProduct(new double[] { 1, 2, 3 }, new double[] { 4, 5, 6 });
Func<double[], double[], double> dotProduct = (x, y) =>
{
var dim = Math.Min(x.Length, y.Length);
var sum = 0.0;
for (var i = 0; i != dim; i++)
sum += x[i] + y[i];
return sum;
};
// var result = matrixVectorProductAsync(...);
Func<double, double, Task<double>> matrixVectorProductAsync = async (x, y) =>
{
var sum = 0.0;
/* do some stuff using await ... */
return sum;
};
从上面的代码中我们可以看出:
当然,关于最后一条,以下这些情况下我们还是可以用 var 关键字的。原因很简单,我们告诉编译器,后面是个什么类型就可以了。
Func<double,double> square = (double x) => x * x;
Func<string,int> stringLengthSquare = (string s) => s.Length * s.Length;
Action<decimal,string> squareAndOutput = (decimal x, string s) =>
{
var sqz = x * x;
Console.WriteLine("Information by {0}: the square of {1} is {2}.", s, x, sqz);
};
现在,我们已经知道 Lambda 的一些基本用法了,如果仅仅就这些东西,那就不叫快乐的 Lambda 表达式了,让我们看看下面的代码。
var a = 5;
Func<int,int> multiplyWith = x => x * a;
var result1 = multiplyWith(10); //50
a = 10;
var result2 = multiplyWith(10); //100
是不是有一点感觉了?我们可以在 Lambda 表达式中用到外面的变量,没错,也就是传说中的闭包啦。
void DoSomeStuff()
{
var coeff = 10;
Func<int,int> compute = x => coeff * x;
Action modifier = () =>
{
coeff = 5;
};
var result1 = DoMoreStuff(compute);
ModifyStuff(modifier);
var result2 = DoMoreStuff(compute);
}
int DoMoreStuff(Func<int,int> computer)
{
return computer(5);
}
void ModifyStuff(Action modifier)
{
modifier();
}
在上面的代码中,DoSomeStuff 方法里面的变量 coeff 实际是由外部方法 ModifyStuff 修改的,也就是说 ModifyStuff 这个方法拥有了访问 DoSomeStuff 里面一个局部变量的能力。它是如何做到的?我们马上会说的 J。当然,这个变量作用域的问题也是在使用闭包时应该注意的地方,稍有不慎就有可能会引发你想不到的后果。看看下面这个你就知道了。
var buttons = new Button[10];
for (var i = 0; i < buttons.Length; i++)
{
var button = new Button();
button.Text = (i + 1) + ". Button - Click for Index!";
button.OnClick += (s, e) => { Messagebox.Show(i.ToString()); };
buttons[i] = button;
}
猜猜你点击这些按钮的结果是什么?是”1, 2, 3…”。但是,其实真正的结果是全部都显示 10。为什么?不明觉历了吧?那么如果避免这种情况呢?
var button = new Button();
var index = i;
button.Text = (i + 1) + ". Button - Click for Index!";
button.OnClick += (s, e) => { Messagebox.Show(index.ToString()); };
buttons[i] = button;
其实做法很简单,就是在 for 的循环里面把当前的 i 保存下来,那么每一个表达式里面存储的值就不一样了。
接下来,我们整点高级的货,和 Lambda 息息相关的表达式(Expression)。为什么说什么息息相关,因为我们可以用一个 Expression 将一个 Lambda 保存起来。并且允许我们在运行时去解释这个 Lambda 表达式。来看一下下面简单的代码:
Expression<Func<MyModel, int>> expr = model => model.MyProperty;
var member = expr.Body as MemberExpression;
var propertyName = member.Expression.Member.Name;
这个的确是 Expression 最简单的用法之一,我们用 expr 存储了后面的表达式。编译器会为我们生成表达式树,在表达式树中包括了一个元数据像参数的类型,名称还有方法体等等。在 LINQ TO SQL 中就是通过这种方法将我们设置的条件通过 where 扩展方法传递给后面的 LINQ Provider 进行解释的,而 LINQ Provider 解释的过程实际上就是将表达式树转换成 SQL 语句的过程。
关于 Lambda 性能的问题,我们首先可能会问它是比普通的方法快呢?还是慢呢?接下来我们就来一探究竟。首先我们通过一段代码来测试一下普通方法和 Lambda 表达 式之间的性能差异。
class StandardBenchmark : Benchmark
{
const int LENGTH = 100000;
static double[] A;
static double[] B;
static void Init()
{
var r = new Random();
A = new double[LENGTH];
B = new double[LENGTH];
for (var i = 0; i < LENGTH; i++)
{
A[i] = r.NextDouble();
B[i] = r.NextDouble();
}
}
static long LambdaBenchmark()
{
Func<double> Perform = () =>
{
var sum = 0.0;
for (var i = 0; i < LENGTH; i++)
sum += A[i] * B[i];
return sum;
};
var iterations = new double[100];
var timing = new Stopwatch();
timing.Start();
for (var j = 0; j < iterations.Length; j++)
iterations[j] = Perform();
timing.Stop();
Console.WriteLine("Time for Lambda-Benchmark: \t {0}ms", timing.ElapsedMilliseconds);
return timing.ElapsedMilliseconds;
}
static long NormalBenchmark()
{
var iterations = new double[100];
var timing = new Stopwatch();
timing.Start();
for (var j = 0; j < iterations.Length; j++)
iterations[j] = NormalPerform();
timing.Stop();
Console.WriteLine("Time for Normal-Benchmark: \t {0}ms", timing.ElapsedMilliseconds);
return timing.ElapsedMilliseconds;
}
static double NormalPerform()
{
var sum = 0.0;
for (var i = 0; i < LENGTH; i++)
sum += A[i] * B[i];
return sum;
}
}
}
代码很简单,我们通过执行同样的代码来比较,一个放在 Lambda 表达式里,一个放在普通的方法里面。通过 4 次测试得到如下结果:
Lambda | Normal-Method |
---|---|
70ms | 84ms |
73ms | 69ms |
92ms | 71ms |
87ms | 74ms |
按理来说,Lambda 应该是要比普通方法慢很小一点点的,但是不明白第一次的时候为什么 Lambda 会比普通方法还快一点。- -!不过通过这样的对比我想至少可以说明 Lambda 和普通方法之间的性能其实几乎是没有区别的。
那么 Lambda 在经过编译之后会变成什么样子呢?让 LINQPad 告诉你。
上图中的 Lambda 表达式是这样的:
Action<string> DoSomethingLambda = (s) =>
{
Console.WriteLine(s);// + local
};
对应的普通方法的写法是这样的:
void DoSomethingNormal(string s)
{
Console.WriteLine(s);
}
上面两段代码生成的 IL 代码呢?是这样地:
DoSomethingNormal:
IL_0000: nop
IL_0001: ldarg.1
IL_0002: call System.Console.WriteLine
IL_0007: nop
IL_0008: ret
<Main>b__0:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: call System.Console.WriteLine
IL_0007: nop
IL_0008: ret
最大的不同就是方法的名称以及方法的使用而不是声明,声明实际上是一样的。通过上面的 IL 代码我们可以看出,这个表达式实际被编译器取了一个名称,同样被放在了当前的类里面。所以实际上,和我们调类里面的方法没有什么两样。下面这张图说明了这个编译的过程:
上面的代码中没有用到外部变量,接下来我们来看另外一个例子。
void Main()
{
int local = 5;
Action<string> DoSomethingLambda = (s) => {
Console.WriteLine(s + local);
};
global = local;
DoSomethingLambda("Test 1");
DoSomethingNormal("Test 2");
}
int global;
void DoSomethingNormal(string s)
{
Console.WriteLine(s + global);
}
这次的 IL 代码会有什么不同么?
IL_0000: newobj UserQuery+<>c__DisplayClass1..ctor
IL_0005: stloc.1
IL_0006: nop
IL_0007: ldloc.1
IL_0008: ldc.i4.5
IL_0009: stfld UserQuery+<>c__DisplayClass1.local
IL_000E: ldloc.1
IL_000F: ldftn UserQuery+<>c__DisplayClass1.<Main>b__0
IL_0015: newobj System.Action<System.String>..ctor
IL_001A: stloc.0
IL_001B: ldarg.0
IL_001C: ldloc.1
IL_001D: ldfld UserQuery+<>c__DisplayClass1.local
IL_0022: stfld UserQuery.global
IL_0027: ldloc.0
IL_0028: ldstr "Test 1"
IL_002D: callvirt System.Action<System.String>.Invoke
IL_0032: nop
IL_0033: ldarg.0
IL_0034: ldstr "Test 2"
IL_0039: call UserQuery.DoSomethingNormal
IL_003E: nop
DoSomethingNormal:
IL_0000: nop
IL_0001: ldarg.1
IL_0002: ldarg.0
IL_0003: ldfld UserQuery.global
IL_0008: box System.Int32
IL_000D: call System.String.Concat
IL_0012: call System.Console.WriteLine
IL_0017: nop
IL_0018: ret
<>c__DisplayClass1.<Main>b__0:
IL_0000: nop
IL_0001: ldarg.1
IL_0002: ldarg.0
IL_0003: ldfld UserQuery+<>c__DisplayClass1.local
IL_0008: box System.Int32
IL_000D: call System.String.Concat
IL_0012: call System.Console.WriteLine
IL_0017: nop
IL_0018: ret
<>c__DisplayClass1..ctor:
IL_0000: ldarg.0
IL_0001: call System.Object..ctor
IL_0006: ret
你发现了吗?两个方法所编译出来的内容是一样的, DoSomtingNormal 和<>c__DisplayClass1.<Main>b__0,它们里面的内容是一样的。但是最大的不一样,请注意了。当我们的 Lambda 表达式里面用到了外部变量的时候,编译器会为这个 Lambda 生成一个类,在这个类中包含了我们表达式方法。在使用这个 Lambda 表达式的地方呢,实际上是 new 了这个类的一个实例进行调用。这样的话,我们表达式里面的外部变量,也就是上面代码中用到的 local 实际上是以一个全局变量的身份存在于这个实例中的。
Lambda 如何实现多态?我们用抽象类和虚方法了,为什么还要用 Lambda 这个玩意?且看下面的代码:
class MyBaseClass
{
public Action SomeAction { get; protected set; }
public MyBaseClass()
{
SomeAction = () =>
{
//Do something!
};
}
}
class MyInheritedClass : MyBaseClass
{
public MyInheritedClass()
{
SomeAction = () => {
//Do something different!
};
}
}
我们的基类不是抽象类,也没有虚方法,但是把属性通过委托的方式暴露出来,然后在子类中重新为我们的 SomeAction 赋予一个新的表达式。这就是我们实现多态的过程,当然父类中的 SomeAction 的 set 有 protected 的保护级别,不然就会被外部随易修改了。但是这还不完美,父类的 SomeAction 在子类中被覆盖之后,我们彻底访问不到它了,要知道真实情况是我们可以通过 base 来访问父类原来的方法的。接下来就是实现这个了:
class MyBaseClass
{
public Action SomeAction { get; private set; }
Stack<Action> previousActions;
protected void AddSomeAction(Action newMethod)
{
previousActions.Push(SomeAction);
SomeAction = newMethod;
}
protected void RemoveSomeAction()
{
if(previousActions.Count == 0)
return;
SomeAction = previousActions.Pop();
}
public MyBaseClass()
{
previousActions = new Stack<Action>();
SomeAction = () => {
//Do something!
};
}
}
上面的代码中,我们通过 AddSomeAction 来实现覆盖的同时,将原来的方法保存在 previousActions 中。这样我们就可以保持两者同时存在了。
大家知道子类是不能覆盖父类的静态方法的,但是假设我们想实现静态方法的覆盖呢?
void Main()
{
var mother = HotDaughter.Activator().Message;
//mother = "I am the mother"
var create = new HotDaughter();
var daughter = HotDaughter.Activator().Message;
//daughter = "I am the daughter"
}
class CoolMother
{
public static Func<CoolMother> Activator { get; protected set; }
//We are only doing this to avoid NULL references!
static CoolMother()
{
Activator = () => new CoolMother();
}
public CoolMother()
{
//Message of every mother
Message = "I am the mother";
}
public string Message { get; protected set; }
}
class HotDaughter : CoolMother
{
public HotDaughter()
{
//Once this constructor has been "touched" we set the Activator ...
Activator = () => new HotDaughter();
//Message of every daughter
Message = "I am the daughter";
}
}
这里还是利用了将 Lambda 表达式作为属性,可以随时重新赋值的特点。当然这只是一个简单的示例,真实项目中并不建议大家这么去做。
实际上这个模式我们在返回方法中已经讲到了,只是没有这样一个名字而已,就算是一个总结吧。故事是这样的,你是不是经常会写到 switch-case 语句的时候觉得不够优雅?但是你又不想去整个什么工厂模式或者策略模式,那怎么样让你的代码看起来高级一点呢?
public Action GetFinalizer(string input)
{
switch
{
case "random":
return () => { /* ... */ };
case "dynamic":
return () => { /* ... */ };
default:
return () => { /* ... */ };
}
}
//-------------------变身之后-----------------------
Dictionary<string, Action> finalizers;
public void BuildFinalizers()
{
finalizers = new Dictionary<string, Action>();
finalizers.Add("random", () => { /* ... */ });
finalizers.Add("dynamic", () => { /* ... */ });
}
public Action GetFinalizer(string input)
{
if(finalizers.ContainsKey(input))
return finalizers[input];
return () => { /* ... */ };
}
好像看起来是不一样了,有那么一点味道。但是一想是所有的方法都要放到那个 BuildFinalizers 里面,这种组织方法实在是难以接受,我们来学学插件开发的方式,让它自己去找所有我们需要的方法。
static Dictionary<string, Action> finalizers;
// 在静态的构造函数用调用这个方法
public static void BuildFinalizers()
{
finalizers = new Dictionary<string, Action>();
// 获得当前运行程序集下所有的类型
var types = Assembly.GetExecutingAssembly().GetTypes();
foreach(var type in types)
{
// 检查类型,我们可以提前定义接口或抽象类
if(type.IsSubclassOf(typeof(MyMotherClass)))
{
// 获得默认无参构造函数
var m = type.GetConstructor(Type.EmptyTypes);
// 调用这个默认的无参构造函数
if(m != null)
{
var instance = m.Invoke(null) as MyMotherClass;
var name = type.Name.Remove("Mother");
var method = instance.MyMethod;
finalizers.Add(name, method);
}
}
}
}
public Action GetFinalizer(string input)
{
if(finalizers.ContainsKey(input))
return finalizers[input];
return () => { /* ... */ };
}
如果要实现插件化的话,我们不光要能够加载本程序集下的方法,还要能随时甚至运行时去加载外部的方法,请继续往下看:
internal static void BuildInitialFinalizers()
{
finalizers = new Dictionary<string, Action>();
LoadPlugin(Assembly.GetExecutingAssembly());
}
public static void LoadPlugin(Assembly assembly)
{
var types = assembly.GetTypes();
foreach(var type in types)
{
if(type.IsSubclassOf(typeof(MyMotherClass)))
{
var m = type.GetConstructor(Type.EmptyTypes);
if(m != null)
{
var instance = m.Invoke(null) as MyMotherClass;
var name = type.Name.Remove("Mother");
var method = instance.MyMethod;
finalizers.Add(name, method);
}
}
}
}
现在,我们就可以用这个方法,给它指定程序集去加载我们需要的东西了。