zoukankan      html  css  js  c++  java
  • C#复习笔记(4)--C#3:革新写代码的方式(Lambda表达式和表达式树)

    Lambda表达式和表达式树

    先放一张委托转换的进化图

    看一看到lambda简化了委托的使用。

    lambda可以隐式的转换成委托或者表达式树。转换成委托的话如下面的代码:

    Func<string, int> getLength = s => s.Length;

    转换成表达式树的话是下面的代码:

     Expression<Func<string, int>> getLength = s => s.Length;

    委托方面的东西前面都做了详细的介绍。我们主要学习表达式树

    表达式树

    表达式是当今编程语言中最重要的组成成分。简单的说,表达式就是变量、数值、运算符、函数组合起来,表示一定意义的式子。例如下面这些都是(C#)的表达式:

    3 //常数表达式
    a //变量或参数表达式
    !a //一元逻辑非表达式
    a + b //二元加法表达式
    Math.Sin(a) //方法调用表达式
    new StringBuilder() //new 表达式
    myString.length//MemberAccess表达式

    首先澄清一个概念:表达式。表达式是一个以;结尾的句子,如果是两句,那就不叫表达式了,比如下面不是表达式:

    {
    ....
    }

    代码作为数据是一个古老的概念,.NET3. 5的表达式树提供了一种抽象的方式将一些代码表示成一个对象树。 它类似于CodeDOM, 但是在一个稍高的级别上操作。 表达式树主要用于LINQ, 本节稍后会解释表达式树对于整个LINQ的重要性。

    System.Linq.Expressions命名空间包含了代表表达式的各个类, 它们都继承自Expression,一个抽象的主要包含一些静态工厂方法的类, 这些方法用于创建其他表达式类的实例。 然而,Expression 类也包括两个属性。

    • Type属性代表表达式求值后的.NET类型, 可把它 视为一个返回类型。 例如,如果一个表达式要获取一个字符串的Length属性, 该表达式的类型就是int。
    • NodeType属性返回所代表的表达式的种类。 它是ExpressionType枚举的成员, 包括LessThan、Multiply和Invoke等。 仍然使用上面的例子,对于myString. Length 这个属性访问来说, 其节点类型是MemberAccess。
     static void Main(string[] args)
            {
                Expression firstArg = Expression.Constant(2);
                Expression secondArg = Expression.Constant(3);
                BinaryExpression add = Expression.Add(firstArg, secondArg);
                Console.WriteLine(add);           
                Console.ReadKey();
            }

    上面的结果最终会输出(2+3).这意味着这些表达式树类覆盖了ToString来产生可读的输出。

    就像代码中所做的,首先创建的是叶表达式,然后自下而上的去创建这个完整的表达式。这是由“ 表达式不易变” 这一事实决定的——创建好表达式后, 它就永远不会改变。 这样就可以随心所欲地缓存和重用表达式。

    LambdaExpression是从Expression派生的类型之一。 泛型类Expression<TDelegate>又是从LambdaExpression派生的。

    Expression和Expression<TDelegate> 类的区别在于, 泛型类以静态类型的方式标识了它是什么种类的表达式,也就是说,它确定了返回类型和参数。 很明显, 这是用TDelegate类型参数来表示的, 它必须是一个委托类型。 例如, 假设我们的简单加法表达式就是一个 不获取任何参数, 并返回整数的委托。 与之匹配的签名就是Func<int>, 所以可以使用一个Expression <Func<int>>, 以静态类型的方式表示该表达式。 我们用Expression.Lambda 方法来完成这件事。 该方法有许多重载版本——我们的例子使用的是泛型方法, 它用一个类型参数来指定我们想要表示的委托的类型。

     static void Main(string[] args)
            {
    
                Expression firstArg = Expression.Constant(2);
                Expression secondArg = Expression.Constant(3);
                BinaryExpression add = Expression.Add(firstArg, secondArg);
                Func<int> lambda = Expression.Lambda<Func<int>>(add).Compile();
                Console.WriteLine(lambda());
                Console.ReadKey();
            }

    Expression.Lambda有泛型的重载,指示表达式可以转换为一个类型实参为Func<int>的Expression,然后,可以通过Compile方法来将表达式树编译成委托。

    印象。我们在程序中创建了一些逻辑块(比如Expression firstArg = Expression.Constant(2);), 将其表示成普通对象, 然后要求框架将所有的东西都编译成可以执行的“真实” 的代码。 你或许永远都不需要真正以这种方式使用表达式树, 甚至永远都不需要在程序中构造 它们,但它提供了相当有用的背景知识,可以帮助你理解LINQ是怎样工作的。

    上面介绍的是将表达式树编译成lambda,下面介绍的是----

    将lambda转换成表达式树

    我们知道,Lambda表达式能显式或隐式地转换成恰当的委托实例。 然而, 这并非唯一能进行的转换。 还可以要求编译器通过你的Lambda 表达式构建一个表达式树, 在执行时创建Expression<TDelegate> 的一个实例。 例如,下面展示了用一种精简得多的方式 创建“返回 5” 的表达式, 然后编译这个表达式, 并调用编译得到的委托。

     static void Main(string[] args)
            {
    
                Expression<Func<int>> return5 = () => 5;
                Func<int> lambda = return5.Compile();
                Console.WriteLine(lambda());
                Console.ReadKey();
            }

    一些限制:

    • 并非**所有** Lambda表达式都能转换成表达式树。 不能将带有一个语句块(即使只有一个return语句) 的Lambda 转换成表达式树—— 只有对单个表达式进行求值的Lambda才可以。
    • 表达式中还不能包含赋值操作,因为在表达式树中表示不了这种操作。 尽管.NET4 扩展了表达式树的功能, 但只能转换单一表达式这一限制仍然有效。 
    • 还有一些其他的限制,不过很少见,你会在编译错误的时候得到编译器的提示

    更复杂的例子:

     static void Main(string[] args)
            {
    
                Expression<Func<string, string, bool>> expression = (x, y) => x.StartsWith(y);
                var lambda = expression.Compile();
                Console.WriteLine(lambda("first that i did","first"));//true
                Console.ReadKey();
            }

    上面这个例子如果用表达式树来做的话是非常复杂的:

     static void Main(string[] args)
            {
               
                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};
                Expression call = Expression.Call(target, method, methodArgs);//②从以上部件构建callexpression
                var lambdaParameters = new[]{target, methodArg};//③将callexpression转化成lambda
                var lambda = Expression.Lambda<Func<string,string,bool>>(call,lambdaParameters);
                var compiled = lambda.Compile();
                Console.WriteLine(compiled("first","second"));//false
                Console.WriteLine(compiled("first", "fir"));//true
                Console.ReadKey();
            }

    首先感谢编译器能够让lambda隐式转化成表达式树!

    唯一的好处是它确实更清晰地展示树中涉及的东西以及参数是如何绑定的。 为了构造最终的方法调用表达式, 我们需要知道方法调用的几个部件①, 其中包括: 方法的目标(也就是调用StartsWith的字符串);方法本身( MethodInfo);参数列表(本例只有一个参数)。在本例中, 方法的目标和参数恰好都是传递给表达式的参数, 但它们完全可能是其他表达式类型, 如常量、其他方法调用的结果、属性的求值结果, 等等。 将方法调用构造成一个表达式之后 ❷, 接着需要把它转换成Lambda表达式 ❸, 并绑定参数。 我们重用 了作为方法调用(部件)信息而创建的参数表达式的值(ParameterExpression): 创建Lambda表达式时指定的参数顺序就是最终调用委托时使用的参数顺序。

    这是编译好之后的表达式树。

    位于linq核心的表达式树

    没有lambda表达式,表达式树几乎没有任何价值。从一定程度上说,没有表达式树,lambda也就没那么有用了。LINQ在C#的全部体现就包括lambda、表达式树和扩展方法这三部分。

    长期以来, 我们要么能在编译时进行很好的检查, 要么能指示另一个平台运行一些代码, 这些指示一般表示成文本(如SQL查询)。 但是,鱼和熊掌不可兼得(这个却是lambda的优点,既能转化成委托,在进程内配合迭代器进行处理集合序列,又能编译成表达式树,以供其他提供器翻译成另一个平台上的语言,比如sql)。 Lambda表达式提供 了编译时检查的能力, 而表达式树可以将执行模型从你所需的逻辑中提取出来。 将两者合并到一起之后, 鱼和熊掌就能兼得 了—— 当然是在一个合理的范畴之内。“ 进程外” LINQ提供器的中心思想在于, 我们可以从一个熟悉的源语言(如 C#) 生成一个表达式树, 将结果作为一个中间格式, 再将其转换成目标平台上的本地语言, 比如SQL。 某些时候, 你更多地会遇到一个本机API, 而不是一种简单的本机语言。 例如, 这个API可能根据表达式所表示的 内容来调用不同的Web服务。下图展示LINQ to Objects 和 LINQ to SQL 的 不同 路径。

     除了LINQ,表达式树也可以用在别的地方

    1、我们在以后的内容中讨论C#动态类型 时, 将看到更多关于动态语言运行时的内容。 表达式树是其架构的核心部分。 它们具有三个特点对DLR特别有吸引力:

    • 它们是不易变的, 因此可以安全地缓存;
    • 它们是可组合的, 因此可以在简单的块中构建出复杂的行为;
    • 它们 可以编译为委托, 后者可以像平常那样进一步JIT 编译为本地代码。

    2、可以放心地对成员的引用进行重构

    以后C#会推出一个infoof的操作符,但具体是干啥的还不知道,这里先做标记。以后来补充。

    3、其他。。。。

    这个回头再来看一下书上的介绍吧。貌似没有用到过

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

    C#3中lambda表达式的加入使得原先的类型推断和重载决策为了新的环境而做了改变。

    用一个Lambda表达式调用一个泛型方法,同时传递一个隐式类型的参数列表,编译器就必须推断出你想要的是什么类型,然后才能检查出Lambda表达式主体。

    static void PrintConvertedValue<TInput, TOutput>(TInput input, Converter<TInput, TOutput> converter)
    {
    Console.WriteLine(converter(input));//C#2中,编译将失败,C#2类型推断单独针对每一个实参来进行的,从一个实参无法推断出另一个实参
    }
    .......
    PrintConvertedValue("hello world",x=>x.length);

    这段代码听说在C#2上面是没有办法通过编译的,但我没办法测试。因为我在用C#7。

    还比如,以下代码如法在C#2通过编译,并提示”The type arguments for method  cannot by inferred from the useage,Try specifying the type argument explicitly."

            delegate T Funcs<out T>();//定义一个委托
    
            static void WriteResult<T>(Funcs<T> func)//向方法传递一个定义好的委托
            {
                Console.WriteLine(func());
            }
            static void Main(string[] args)
            {
                WriteResult(() => 5);//error:”The type arguments for method  cannot by inferred from the useage,Try specifying the type argument explicitly."
                Console.ReadKey();
            }

    在C#2中编译器建议你显示的为方法指定一个类型实参或强制转换:

    WriteResult< int>( delegate { return 5; }); 
    WriteResult(( MyFunc< int>) delegate { return 5; });

    我们希望编译器能像对非委托类型所做的那样, 执行相同的类型推断, 也就是根据返回的表达式的类型来推断T的类型。 那正是 C# 3为匿名方法和Lambda表达式所做的事情—— 但其中存在一个陷阱。

    delegate T Funcs<out T>();
    
            static void WriteResult<T>(Funcs<T> func)
            {
                Console.WriteLine(func());
            }
            static void Main(string[] args)
            {
                WriteResult(delegate()
                {
                    if (DateTime.Now.Hour<12)
                    {
                        return 10;
                    }
                    return new object();
                });
                Console.ReadKey();
            }

    在这种情况下, 编译器采用和处理隐式类型的数组时相同的逻辑来确定返回类型,比如var objs = new[] {new object(), "hello world"};。 它构造一个集合,其中包含了来自匿名函数主体中的return 语句的所有类型 (本例是int和object), 并检查是否集合中的所有类型都能隐式转换成其中的一个类型。 int 到object 存在一个隐式转换(通过装箱),但object 到 int 就不存在了。 所以,object被推断为返回类型。 如果没有找到符合条件的类型, 或者找到了多个, 就无法推断出返回类型, 编译器会报错。

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

    在这里, 我们打算以一种较为“笼统” 的方式来思考类型推断—— 效果和你粗读一下C# 语言规范差不多, 但我们的方式会更容易理解。 事实上,假如编译器不能完全按照你希望的方式执行类型推断, 最后几乎肯定会造成一个编译错误, 而不会生成一个行为不正确的程序。如果你的代码未能成功编译, 请尝试向编译器提供更多的信息——就那么简单。 然而, 我下面仍然要大致地解释一下C# 3发生的改变。

    第一个巨大的改变是所有方法实参在C# 3中是一个“ 团队” 整体。在C# 3中, 实参可提供一些信息—— 被强制隐式转换为具体类型参数的最终固定变量的类型。 用于推断固定值所采用的逻辑与推断返回类型和隐式类型的数组是一样的。

    下面展示了一个例子—— 没有使用任何Lambda表达式, 就连匿名方法都没用。不存在协变逆变,以及方法组转换之类的。就是单纯的类型推断

     static void TestTypeParameter<T>(T firstValue, T secondValue)
            {
                Console.WriteLine(typeof(T));
            }
            static void Main(string[] args)
            {
               TestTypeParameter(1,new object());
               Console.ReadKey();
            }

    第二个改变在于, 类型推断现在是分两个阶段进行的。 第一个阶段处理的是“普通” 的实参, 其类型是一开始便知道的。 这包括那些参数列表是显式类型的匿名函数。

    稍后进行的第二个阶段是推断隐式类型的Lambda表达式和方法组的类型。 其思想是, 根据我们迄今为止拼凑起来的信息, 判断是否足够推断出Lambda表达式(或方法组) 的参数类型。 如果能, 编译器就可以检查Lambda 表达式的主体并推断返回类型—— 这个返回 类型通常能帮助我们确定当前正在推断的另一个类型参数。 如果第二个阶段提供了更多的信息, 就重复执行上述过程, 直到我们用光了所有线索, 或者最终推断出涉及的所有类型参数。

    static void PrintConvertedValue< TInput, TOutput> (TInput input, Converter< TInput, TOutput> converter) 
    {
    Console. WriteLine( converter( input));
    }
    ...
    PrintConvertedValue(" I' m a string", x => x. Length);

    还是使用上面的那个代码,来说明:

    1、阶段1开始。

    2、第1个参数是TInput类型, 第1个实参是string类型。 我们推断出肯定存在从string 到TInput的隐式转换。

    3、第2个参数是 Converter< TInput, TOutput> 类型, 第2个实参是一个隐式类型的Lambda表达式。 此时不执行任何推断, 因为我们没有掌握足够的信息。

    4、阶段2开始。

    5、TInput不依赖任何非固定的类型参数, 所以它被确定为string。

    6、第2个实参现在有一个固定的输入类型, 但有一个非固定的输出类型。 我们可把它视为( string x) => x. Length, 并推断出其返回类型是int。 因此, 从int到TOutput 必定会发生一个隐式转换。

    7、重复“ 阶段 2”。

    8、TOutput不依赖任何非固定的类型参数,所以它被确定为int。

    9、完成了。

    再看一下下面这个:多级推断

     static void Main(string[] args)
            {
               ConvertTwice("AnotherString",x=>x.Length, length => Math.Sqrt(length));
               Console.ReadKey();
            }
    
            static void ConvertTwice<TInput, TMiddle, TOutput>(TInput input, 
                Converter<TInput, TMiddle> firstConverter,
                Converter<TMiddle, TOutput> secondConverter)
            {
                TMiddle middle = firstConverter(input);
                TOutput output = secondConverter(middle);
                Console.WriteLine(output);
            }

    类型推断的“阶段1” 告诉编译器肯定存在从string到TInput 的一个转换。 第一次执行“ 阶段 2” 时, TInput固定为string, 我们推断肯定存在从int到TMiddle的一个转换。 第二次执行“ 阶段 2” 时, TMiddle固定为int, 我们推断肯定存在从double到TOutput的一个转换。 第三次执行“阶段2” 时, TOutput 固定 为 doluble, 类型推断成功。 当类型推断结束后, 编译器就可以正确地理解Lambda表达式中的代码。

     注意:Lambda 表达式的主体只有在输入参数的类型已知之后才能进行检查。上面的例子中,只有编译器只有等到阶段1完事儿了,判断出TInput的类型是string之后,才会进入第二阶段,对第一个Converter进行检查。以此类推。

    C#3选择正确的被重载的方法(方法组)

    首先看一下直接调用方法组

    有一个有趣的规则是“更好的转换”规则,拿个例子来说:

    void Write( int x) ;
    void Write( double y);

    Write( 1.5) 的含义显而易见, 因为不存在从 double 到 int 的 隐式 转换, 但 Write( 1) 对应的调用就麻烦一些。 由于存在从 int 到 double 的隐式转换, 所以以上两个方法似乎都合适。 在这种情况下, 编译器会考虑从int 到 int 的转换, 以及从 int 到 double 的转换。 从 任何类型“ 转换成它本身” 被认为好于“ 转换成一个不同的类型”。 这个规则称为“ 更好的转换” 规则。 所以对于这种特殊的调用, Write( int x) 方法被认为好于Write( double y)。

    委托返回类型影响了重载选择

     static void Execute(Func<int> action)
            {
                Console.WriteLine($"method returns a int:{action()}");
            }
            static void Execute(Func<double> action)
            {
                Console.WriteLine($"method returns a double:{action()}");
            }
            static void Main(string[] args)
            {
                Execute(()=> 4.3);//①
    Execute(()=>4);//② Console.ReadKey(); }

    对Execute的调用可以换用一个匿名方法来写, 也可以换用一个方法组—— 不管以什么方式, 凡是涉及转换, 所应用的规则都是一样的。同样这个代码如果在C#2中应用的话编译器还是会报错,但是现在添加了新的语言规则:‘

    如果一个匿名函数能转换成参数列表相同, 但返回类型不同的两个委托类型, 就根据从“ 推断 的返回类型”到“委托的返回类型” 的转换来判定哪个委托转换“ 更好”。

    总结以下本节的重点:

    • 匿名函数(匿名方法和Lambda表达式) 的返回类型是根据所有return语句的类型来推断的;
    • Lambda表达式要想被编译器理解, 所有参数的类型必须为已知;
    • 类型推断不要求根据不同的(方法) 实参推断出的类型参数的类型完全 一致, 只要推断出来的结果是兼容的就好;
    • 类型推断现在分阶段进行, 为一个匿名函数推断的返回类型可作为另一个匿名函数的参数类型使用;
    • 涉及匿名函数时, 为了找出“ 最好” 的重载方法, 要将推断的返回类型考虑在内。
  • 相关阅读:
    APUE学习笔记:第四章 文件和目录
    APUE学习笔记:第三章 文件I/O
    APUE学习笔记:第二章 UNIX标准化及实现
    APUE学习笔记:第一章 UNUX基础知识
    《数据库系统概念》学习笔记2
    go语言下载地址
    Centos7 编译 android4.4
    剑侠情缘新进展
    ubuntu server 14.04手动安装svn
    ubuntu server 14.04 lts显示乱码的问题
  • 原文地址:https://www.cnblogs.com/pangjianxin/p/8669313.html
Copyright © 2011-2022 走看看