C# in depth (第九章 Lambda表达式和表达式树)

LINQ 的基本功能就是创建操作管道,以及这些操作需要的任何状态。 这些操作表示了各种关于数据的逻辑:如何过滤,如何排序以及如何将不同的数据源连接在一起。

9.1 作为委托的Lambda表达式

  • 从许多方面,Lambda表达式都可以看作是C#2.0的匿名方法的一种演变。匿名方法能做的几乎一切事情都可以用Lambda表达式来完成。
  • 表达式的类型本身并非委托类型,但它可以通过多种方式隐式或显示地转换成一个委托实例。

9.1.1 准备工作: Func<...>委托类型简介

如果我们需要获取一个string参数,并返回一个int, 所以我们将使用 Func<string, int>

 

9.1.2 第一次转换成Lambda表达式

用匿名方法来创建委托

  class SimpleAnonymousMethod
    {
        static void Main()
        {
            Func<string, int> returnLength;
            returnLength = delegate(string text) { return text.Length; };

            Console.WriteLine(returnLength("Hello"));
        }
    }

冗长的第一个Lambda表达式,和匿名方法相似  (可以将=>读成 goes to )

  class FirstLambdaExpression
    {
        static void Main()
        {
            Func<string, int> returnLength;
            returnLength = (string text) => { return text.Length; };

            Console.WriteLine(returnLength("Hello"));
        }
    }

 

9.1.3 用单一表达式作为主体

 (string text) => { return text.Length; };

可以转变为

 (string text) =>  text.Length; 

 

9.1.4 隐式类型的参数列表

编译器大多数时候都能猜出参数类型,不需要你显示声明它们。在这些情况下,可以将Lambda表达式写成:

(隐式类型的参数列表)=>表达式

 

(string text) =>  text.Length; 
可以转换为
(text) =>text.Length;

 

9.1.5 单一参数的快捷与法

最终可以把那个小括号也去掉

    class FirstLambdaExpression
    {
        static void Main()
        {
            Func<string, int> returnLength;
            returnLength = (string text) => { return text.Length; };

            Console.WriteLine(returnLength("Hello"));
        }
    }

 

9.2 使用List<T>和事件的简单例子

9.2.1 列表的过滤,排序和操作

  class FilmFilteringAndSorting
    {
        class Film
        {
            public string Name { get; set; }
            public int Year { get; set; }
        }

        static void Main()
        {
            var films = new List<Film>
            {
                new Film {Name="Jaws", Year=1975},
                new Film {Name="Singing in the Rain", Year=1952},
                new Film {Name="Some like it Hot", Year=1959},
                new Film {Name="The Wizard of Oz", Year=1939},
                new Film {Name="It's a Wonderful Life", Year=1946},
                new Film {Name="American Beauty", Year=1999},
                new Film {Name="High Fidelity", Year=2000},
                new Film {Name="The Usual Suspects", Year=1995}
            };

            Action<Film> print = 
                film => Console.WriteLine("Name={0}, Year={1}", film.Name, film.Year);

            // Note: extra lines added for clarity when running
            Console.WriteLine("All films");
            films.ForEach(print);                                
            Console.WriteLine();

            Console.WriteLine("Oldies");
            films.FindAll(film => film.Year < 1960)              
                 .ForEach(print);
            Console.WriteLine();

            Console.WriteLine("Sorted");
            films.Sort((f1, f2) => f1.Name.CompareTo(f2.Name));  
            films.ForEach(print);                                
        }        
    }

 9.2.2 在事件处理程序中进行记录

class EventLogging
    {
        static void Log(string title, object sender, EventArgs e)
        {
            Console.WriteLine("Event: {0}", title);
            Console.WriteLine("  Sender: {0}", sender);
            Console.WriteLine("  Arguments: {0}", e.GetType());
            foreach (PropertyDescriptor prop in
                     TypeDescriptor.GetProperties(e))
            {
                string name = prop.DisplayName;
                object value = prop.GetValue(e);
                Console.WriteLine("    {0}={1}", name, value);
            }
        }

        static void Main()
        {
            Button button = new Button();
            button.Text = "Click me";
            button.Click += (src, e) => Log("Click", src, e);
            button.KeyPress += (src, e) => Log("KeyPress", src, e);
            button.MouseClick += (src, e) => Log("MouseClick", src, e);

            Form form = new Form();
            form.AutoSize = true;
            form.Controls.Add(button);
            Application.Run(form);
        }
    }

9.3 表达式树

9.3.1 以编程方式构建表达式树

  • 表达式树是对象构成的树,数中每个节点本身都是一个表达式。不同的表达式类型代表能在代码中执行的不同操作:二元操作(例如加法),一元操作(例如获取一个数组的长度),方法调用,构造函数调用,等等。
  • System.Linq.Expressions命名空间包含了代表表达式的各个类,它们都继承自Expression,一个抽象的主要包含一些静态工厂方法的类,这些方法用于创建其他表达式的实例。然而,Expression类也包括两个属性。
  1. Type属性代表表达式求值后的.Net类型可把它视为一个返回类型。例如,如果一个表达式要获取一个字符串的Length属性,该表达式的类型就是int。
  2. NodeType属性返回所代表的表达式的种类它是ExpressionType枚举的成员,包括LessThan, Multipy和Invoke等。 仍然使用上面的例子,对于mystring.Length这个属性访问来说,其节点类型是MemberAccess
   class FirstExpressionTree
    {
        static void Main()
        {
            Expression firstArg = Expression.Constant(2);
            Expression secondArg = Expression.Constant(3);
            Expression add = Expression.Add(firstArg, secondArg);
           
            Console.WriteLine(add);
        }
    }

 

 

add

BinaryExpression

NodeType =Add

Type= System.Int32

 

firsttArg

ConstantExpression

NodeType=Constant

Type=System.Int32

Value=2

 

secondArg

ConstantExpression

NodeType=Constant

Type=System.Int32

Value=3

 9.3.2 将表达式树编译成委托

(继承关系)

  Expression  
LambdaExpression BinaryExpression 其他类型(ConstantExpression)
Expression<TDelegate>    
  • Expression 和 Expression<TDelegate>类的区别在于,泛型类以静态类型的方式标示了它是什么种类的表达式,也就是说,它确定了返回类型的参数。很明显TDelegate必须是一个委托
  • 那么,这样做的意义何在呢?LambdaExpression有一个Compile方法能创建恰当类型的委托。Expression<TDelegate>也有一个同名的方法,但它静态类型化后返回TDelegate类型的委托。该委托现在可以采用普通方式执行,就好像它是用一个普通方法或者其他方式来创建的一样。
 class CompiledExpressionTree
    {
        static void Main()
        {
            Expression firstArg = Expression.Constant(2);
            Expression secondArg = Expression.Constant(3);
            Expression add = Expression.Add(firstArg, secondArg);

            Func<int> compiled = Expression.Lambda<Func<int>>(add).Compile();

            Console.WriteLine(compiled());
        }
    }

 9.3.3  将C# Lambda表达式转换成表达式树

    class LambdaExpressionToExpressionTree
    {
        static void Main()
        {
            Expression<Func<int>> return5 = () => 5;//编译器可以自动将lambda表达式转换成表达式树。
Func
<int> compiled = return5.Compile(); Console.WriteLine(compiled()); } }
  •  有一些限制:
  1. 不能将带有一个语句块(即使只有一个return语句)的lambda表达式转换成表达式树

表达式中还不能包含赋值操作,因为在表达式树中表示不了这种操作。

class LambdaExpressionWithParametersToExpressionTree
    {
        static void Main()
        {
            Expression<Func<string, string, bool>> expression = (x, y) => x.StartsWith(y);

            var compiled = expression.Compile();

            Console.WriteLine(compiled("First", "Second"));
            Console.WriteLine(compiled("First", "Fir"));
        }
    }

 用代码来构造一个方法调用表达式树

class MethodCallExpressionTree
    {
        static void Main()
        {
            MethodInfo method = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });
            var target = Expression.Parameter(typeof(string), "x");
            var methodArg = Expression.Parameter(typeof(string), "y");                         
Expression[] methodArgs
= new[] { methodArg }; //1.以上为构造方法调用的各个部件 Expression call = Expression.Call(target, method, methodArgs); //2.从以上部件创建CallExpression
var lambdaParameters = new[] { target, methodArg }; var lambda = Expression.Lambda<Func<string, string, bool>>(call, lambdaParameters); var compiled = lambda.Compile(); //3.以上将Call转换成Lambda表达式 Console.WriteLine(compiled("First", "Second")); Console.WriteLine(compiled("First", "Fir")); } }
  1.  为了构造最终的方法调用表达式,我们需要知道方法调用的几个部件(1.) 其中包括: 方法的目标(也就是调用StartsWith的字符串);方法本身(MethodInfo);参数列表(本例只有一个参数)。
  2. 将方法调用构造成一个表达式之后(2.),接着需要把它转换成Lambda表达式(3.),并绑定参数。

     P.217 图9-4

9.3.4 位于LINQ核心的表达式树

  • Lambda表达式提供了编译时检查的能力,而表达式树可以将执行模型从你所需的逻辑中提取出来。
  • LINQ提供器的中心思想在于,我们可以从一个熟悉的源语言(如C#)生成一个表达式树,将结果作为一个中间格式,再将其转换成目标平台上的本地语言。比如SQL。
 

编译时 

 
含有Lambada表达式的C#查询代码 含有Lambada表达式的C#查询代码

                  (C#编译器)

(C#编译器)
使用委托的IL 使用表达式树的IL

 

 执行时

委托代码直接在CLR中执行

                    (LINQ to SQL 提供器)
              动态SQL
                         (在数据库处执行,并取回结果)
查询结果    查询结果
 LINQ to Objects LINQ to SQL 

                               无论LINQ to Objects还是LINQ to SQL都是始于C#代码,结束于查询结果。表达式树提供了远程执行代码的能力。

  • 编译器并不能做所有的检查 :例如:索然可以将string.StartWith调用转换成类似的SQL表达式,但string.IsInterned的调用在数据库环境中是没有意义的。表达式树确保了大量编译时安全性,但编译器只能检查以确保Lambda表达式能转换成一个有效的表达式树,它不能保证表达式树最后的使用是否合适。

 

9.3.5 LINQ之外的表达式树

  1. 优化动态语言运行时
  2. 可以放心地对成员的引用进行重构
  3. 更简单的反射

9.4 类型推断和重载决策的改变

9.4.1 改变的起因

需要新的类型推断规则的例子:

 class ConversionWithTypeInference
    {
        static void PrintConvertedValue<TInput, TOutput>
            (TInput input, Converter<TInput, TOutput> converter)
        {
            Console.WriteLine(converter(input));
        }

        static void Main()
        {
            PrintConvertedValue("I'm a string", x => x.Length);
        }
    }
  • 第一个实参明显是个字符串,但第二个呢?是个Lambda表达式,所以需要把它转换成一个Converter<TInput, TOutput>,而那意味着要知道TInput和TOutput的类型。所以如果还是沿用C#2.0的规则,代码清单9-11的代码就会编译失败。

9.4.2 推断匿名函数的返回类型

以下代码清单展示了貌似能编译,但不符合C#2类型推断规则的示例代码 (其实是可以编译的???)

    class ReturnTypeInference
    {
        delegate T MyFunc<T>();

        static void WriteResult<T>(MyFunc<T> function)
        {
            Console.WriteLine(function());
        }

        static void Main()
        {
            WriteResult(delegate { return 5; });
        }
    }

代码清单9-13 根据一天当中的时间来选择返回int或object

class ReturnTypeInferenceWithMultipleReturns
    {
        delegate T MyFunc<T>();

        static void WriteResult<T>(MyFunc<T> function)
        {
            Console.WriteLine(function());
        }

        static void Main()
        {
            WriteResult(delegate
            {
                if (DateTime.Now.Hour < 12)
                {
                    return 10;
                }
                else
                {
                    return new object();
                }
            });
        }
    }
  • 在这种情况下,编译器采用和处理隐式类型的数组时相同的逻辑来确定返回类型。int到object存在一个隐式转换,但object到int就不存在了。所以object被推断为返回类型。

9.4.3 分两个阶段进行的类型推断

综合来自多个实参的信息,灵活地进行推断

class MultipleArgumentInference
    {
        static void PrintType<T>(T first, T second)
        {
            Console.WriteLine(typeof(T));
        }

        static void Main()
        {
            PrintType(1, new object());
        }
    }
  • 和上一个例子一样,这里T会被推断为Object
  • 类型推断现在是分两个阶段进行的。
  1. 第一个阶段处理的是“普通“的实参,其类型是一开始便知道的。
  2. 第二个阶段是推断隐式类型的Lambda表达式和方法组的类型,其思想是根据我们迄今为止拼凑起来的信息,判断是否足够推断出Lambda表达式(或方法组)的参数类型。

9-7用流程图展示了这一过程。(P.225)

   class MultiStageInference
    {
        static void ConvertTwice<TInput, TMiddle, TOutput> (TInput input,
                                                            Converter<TInput, TMiddle> firstConversion,
                                                            Converter<TMiddle, TOutput> secondConversion)
        {
            TMiddle middle = firstConversion(input);
            TOutput output = secondConversion(middle);
            Console.WriteLine(output);
        }

        static void Main()
        {
            ConvertTwice("Another string",
                         text => text.Length,
                         length => Math.Sqrt(length));
        }
    }

9.4.4 选择正确的被重载的方法。

  • void Write(int x)  void Write(double y)  如果 Write(1) 会调用 Write(int x) 这个规则叫”更好的转换“规则
  class OverloadingByDelegateReturnType
    {
        static void Execute(Func<int> action)
        {
            Console.WriteLine("action returns an int: " + action());
        }

        static void Execute(Func<double> action)
        {
            Console.WriteLine("action returns a double: " + action());
        }

        static void Main()
        {
            Execute(() => 1);  //会调用第一个Execute函数
        }
    }

 9.4.5 类型推断和重载决策

9.5 小结

  • 我们知道Lambda表达式并非仅仅是创建委托的一种更精简的语法。它们能转换成表达式树,然后可由其他代码处理,从而在不同的环境中执行等价的行为。如果没有这项功能,LINQ就仅限于进程内查询。
posted @ 2015-08-27 17:44  莱茵哈特  阅读(238)  评论(0编辑  收藏  举报