Lambda.md 18 KB

Lambda

了解 Lambda

在.NET 1.0 的时候,大家都知道我们经常用到的是委托。有了委托呢,我们就可以像传递变量一样的传递方法。在一定程序上来讲,委托是一种强类型的托管的方法指针,曾经也一时被我们用的那叫一个广泛呀,但是总的来说委托使用起来还是有一些繁琐。来看看使用一个委托一共要以下几个步骤:

  1. 用 delegate 关键字创建一个委托,包括声明返回值和参数类型
  2. 使用的地方接收这个委托
  3. 创建这个委托的实例并指定一个返回值和参数类型匹配的方法传递过去

后来,幸运的是.NET 2.0 为了们带来了泛型。于是我们有了泛型类,泛型方法,更重要的是泛型委托。最终 在.NET3.5 的时候,我们 Microsoft 的兄弟们终于意识到其实我们只需要 2 个泛型委托(使用了重载)就可以覆盖 99%的使用场景了。

  • Action 没有输入参数和返回值的泛型委托
  • Action 可以接收 1 个到 16 个参数的无返回值泛型委托
  • Func 可以接收 0 到 16 个参数并且有返回值的泛型委托
  • 这样我们就可以跳过上面的第一步了,不过第 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;
    };
    

    从上面的代码中我们可以看出:

    • 如果只有一个参数,不需要写()
    • 如果只有一条执行语句,并且我们要返回它,就不需要{},并且不用写 return
    • Lambda 可以异步执行,只要在前面加上 async 关键字即可
    • Var 关键字在大多数情况下都不能使用

    当然,关于最后一条,以下这些情况下我们还是可以用 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 性能的问题,我们首先可能会问它是比普通的方法快呢?还是慢呢?接下来我们就来一探究竟。首先我们通过一段代码来测试一下普通方法和 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 告诉你。

    LINQ

    上图中的 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 代码我们可以看出,这个表达式实际被编译器取了一个名称,同样被放在了当前的类里面。所以实际上,和我们调类里面的方法没有什么两样。下面这张图说明了这个编译的过程:

    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 实际上是以一个全局变量的身份存在于这个实例中的。

    IL

    Lambda 表达式玩转多态

    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);
                }
            }
        }
    }
    

    现在,我们就可以用这个方法,给它指定程序集去加载我们需要的东西了。