勇哥的C#知识经验圈点:Lambda 表达式

勇哥注:

这个系列的贴子主要是为了培训用(专职自动化设备的C#软件工程师),因此例子的知识范围被限制在行业范围内。

C#基础知识在网上是最多的存在,不足主要下面几点:

1. 内容零碎,没有大纲来组织
2. 什么功能都有讲,就是没有按实际项目的常用程度来组织
3. PLC转上位机的人员,很难把PLC的编程思想转到C#编程上来,需要在知识点讲解上对此问题有点拔

勇哥的这套贴子会有大纲,主要特点是补足以上几点问题,另外本套贴子属于经验性质的圈点知识,不属于完全小白式的教学。

如果读者有好的意见,请留言反馈。


Lambda表达式

C#的Lambda表达式是一种简洁的函数定义方式,它可以用来表示匿名函数,简化代码,提高可读性。
它的语法如下:
(Input Param)=>Expression
主要的结构就是 参数 =>(goes to) 代码块


简单例子:

假设你有一个字符串数组,里面有很多字符串,你想要筛选出长度大于3的字符串,
你可以使用Lambda表达式来实现:

string[] strings = {"cat", "dog", "apple", "banana"};

// 使用Lambda表达式
string[] result = strings.Where(str => str.Length > 3).ToArray();

// 输出:"apple", "banana"


目录:

(1).net历史版本如何表示一个匿名函数

(2)作为委托的Lambda表达式

(3)Lambda表达式转换为表达式树



(1).net历史版本如何表示一个匿名函数

Lambda表达式是一个不断进化的东西,它的功能是年代的进化与积累。

    public class LambdaTest
    {
        public void Show(int a1, string b1)
        {
            Console.WriteLine($"show{a1}:{b1}");
        }

        public void test()
        {
            //1. .net1.0/1.1 原始方法
            ShowDelegate showDelegate = new ShowDelegate(Show);
            //2. .net 2.0 匿名方法,增加delegate,去掉单独定义方法
            showDelegate = delegate (int x, string y) { Console.WriteLine($"show{x}:{y}"); };
            //3. .net3.0 =>引入Lambda表达式,去掉delegate
            showDelegate = (int s, string t) => { Console.WriteLine($"show{s}:{t}"); };
            //4. .net3.0后期,简化参数类型,编译器自动推断数据类型
            showDelegate = (s, t) => { Console.WriteLine($"show{s}:{t}"); };
            //5. 如果方法体只有一行代码,可以省略方法体大括号
            showDelegate = (s, t) => Console.WriteLine($"show{s}:{t}");
            //如果方法只有一个参数,可以省略参数小短号
            Action<string> action = x => Console.WriteLine($"show{x}");
            //如果方法体中只有一行代码,且有返回值,可以省略return
            Func<int, string> func = x => x.ToString();
        }
    }


(2)作为委托的Lambda表达式

这是Lambda表达式最常见的应用。

示例1:求字符串长度

       /// <summary>
        /// 作为委托的Lambda表达式
        /// </summary>
        public void test2()
        {
            //使用C# 2.0中的匿名方法获取字符串长度
            Func<string, int> strLength = delegate (string str) { return str.Length; };
            Console.WriteLine(strLength("Hello World!"));

            //使用Lambda表达式
            //(显式类型参数列表)=> {语句},lambda表达式最冗长版本
            strLength = (string str) => { return str.Length; };
            Console.WriteLine(strLength("Hello World!"));

            //单一表达式作为主体
            //(显式类型参数列表)=> 表达式
            strLength = (string str) => str.Length;
            Console.WriteLine(strLength("Hello World!"));

            //隐式类型的参数列表
            //(隐式类型参数列表)=> 表达式
            strLength = (str) => str.Length;
            Console.WriteLine(strLength("Hello World!"));

            //单一参数的快捷语法
            //参数名 => 表达式
            strLength = str => str.Length;
            Console.WriteLine(strLength("Hello World!"));
        }

"=>"是C# 3.0新增的,告诉编译器我们正在使用Lambda表达式。"=>"可以读作"goes to",所以例子中的Lambda表达式可以读作"str goes to str.Length"。从例子中还可以看到,根据Lambda使用的特殊情况,我们可以进一步简化Lambda表达式。

Lambda表达式大多数时候都是和一个返回非void的委托类型配合使用(例如Func<TResult>)。在C# 1.0中,委托一般用于事件,很少会返回什么结果。在LINQ中,委托通常被视为数据管道的一部分,接受输入并返回结果,或者判断某项是否符合当前的筛选器等等。


示例2:List<t>.FindIndex方法

        public void test3()
        {
            List<string> list1 = new List<string>();

            list1.Add("abc1"); list1.Add("abc2"); list1.Add("abc3");
            list1.Add("abc4"); list1.Add("abc5"); list1.Add("abc6");
            var idx = list1.FindIndex(s => s == "abc4");
            MessageBox.Show(idx.ToString());
        }

它的函数命名如下:

其中Predicate<string> match是个委托。

image.png

 这个Predicate<string>是个泛型委托,原型见下面。

image.png


示例3:List<T>中其它的Lambda表达式

 public class Book
    {
        public string Name { get; set; }
        public int Year { get; set; }
    }

public void test4()
        {
            var books = new List<Book>
            {
                new Book{Name="C# learning guide",Year=2005},
                new Book{Name="C# step by step",Year=2005},
                new Book{Name="Java learning guide",Year=2004},
                new Book{Name="Java step by step",Year=2004},
                new Book{Name="Python learning guide",Year=2003},
                new Book{Name="C# in depth",Year=2012},
                new Book{Name="Java in depth",Year=2014},
                new Book{Name="Python in depth",Year=2013},
            };

            //创建一个委托实例来表示一个通用的操作
            Action<Book> printer = book => Console.WriteLine("Name = {0}, Year = {1}", book.Name, book.Year);

            books.ForEach(printer);

            //使用Lambda表达式对List<T>进行筛选
            books.FindAll(book => book.Year > 2010).ForEach(printer);

            books.FindAll(book => book.Name.Contains("C#")).ForEach(printer);

            //使用Lambda表达式对List<T>进行排序
            books.Sort((book1, book2) => book1.Name.CompareTo(book2.Name));
            books.ForEach(printer);

            Console.Read();
        }

从上面例子可以看到,当我们要经常使用一个操作的时候,我们最好创建一个委托实例,然后反复调用,而不是每次使用的时候都使用Lambda表达式(例如例子中的printer委托实例)。

相比C# 1.0中的委托或者C# 2.0的匿名函数,结合Lambda表达式,对List<T>中的数据操作变得简单,易读。


示例4:在线程中使用


        private void button4_Click(object sender, EventArgs e)
        {
            Task.Factory.StartNew(() =>
            {
                for(int i=0;i<10;i++)
                {
                    Console.WriteLine(i * 5);
                }
            });
        }

StartNew的一个参数是Action是个无返回值的委托。

image.png


我们来看下第4个重载,是个泛型的Action,和一个object。

这个该如何调用呢?

image.png

调用方式如下:

 string data = "hello";
 Task.Factory.StartNew((s) =>
 {
     for (int i = 0; i < 10; i++)
     {
         Console.WriteLine($"{data}{i.ToString()}");
     }
 },data);


那第12个重载方法怎么调用呢?

image.png

我们知道对于Func泛型委托来说,最后一个TResult是返回值,其它的是传入值,本例传入值是object,那么state就是那个传入的值了。

如果像下面这样不用Lambda表达式,比较容易调用:

        {
           Func<object, int> fun1 = fun2;
           Task.Factory.StartNew(fun1, data2);
        }

        private int fun2(object  data)
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine($"{data}{i.ToString()}");
            }
            return 0;
        }

那改成Lambda表达式怎么弄呢?

            //使用Labmda表达式
            Task.Factory.StartNew(new Func<object, int>((y) =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine($"{data2}{i.ToString()}");
                }
                return 0;
            }),data2);

注意跟Action<T>比起来,用法只是函数体内多了个return语句。


示例5:List<T>的排序

下面是Sort的第2个重载,该怎么调用呢?

image.png

这个重载的原型是下面这样的:

//
        // 摘要:
        //     使用指定的 System.Comparison`1,对整个 System.Collections.Generic.List`1 中的元素进行排序。
        //
        // 参数:
        //   comparison:
        //     比较元素时要使用的 System.Comparison`1。
        //
        // 异常:
        //   T:System.ArgumentNullException:
        //     comparison 为 null。
        //
        //   T:System.ArgumentException:
        //     comparison 的实现导致排序时出现错误。 例如,将某个项与其自身比较时,comparison 可能不返回 0。
        public void Sort(Comparison<T> comparison);

其中的Comparison是一个委托。

image.png

更详细的原型如下 :

image.png

调用方法如下:

CountData.Sort(new Comparison<Tuple<string, int>>((x1, x2) =>
{
    if (x1.Item2 < x2.Item2) return -1;
    else if (x1.Item2 > x2.Item2) return 1;
    else return 0;
}));
CountData.Reverse();


至此,读者应该对于Lambda表达用来表示委托已经很熟练了。


(3)Lambda表达式转换为表达式树

注意,此话题仅做为延伸阅读。给好奇心强的童鞋准备。


表达式树也称表达式目录树,将代码以一种抽象的方式表示成一个对象树,树中每个节点本身都是一个表达式。表达式树不是可执行代码,它是一种数据结构。

下面我们看看怎么通过C#代码建立一个表达式树

构建表达式树

System.Linq.Expressions命名空间中包含了代表表达式的各个类,所有类都从Expression派生,我们可以通过这些类中的静态方法来创建表达式类的实例。Expression类包括两个重要属性:

  • Type属性代表求值表达式的.NET类型,可以把它视为一个返回类型

  • NodeType属性返回所代表的表达式的类型

下面看一个构建表达式树的简单例子:

Expression numA = Expression.Constant(6);
Console.WriteLine("NodeType: {0}, Type: {1}", numA.NodeType, numA.Type);
Expression numB = Expression.Constant(3);
Console.WriteLine("NodeType: {0}, Type: {1}", numB.NodeType, numB.Type);

BinaryExpression add = Expression.Add(numA, numB);
Console.WriteLine("NodeType: {0}, Type: {1}", add.NodeType, add.Type);

Console.WriteLine(add);
Console.Read();

代码的输出为:

通过例子可以看到,我们构建了一个(6+3)的表达式树,并且查看了各个节点的Type和NodeType属性。

Expression有很多派生类,有很多节点类型。例如,BinaryExpression就代表了具有两个操作树的任意操作。

这正是NodeType属性重要的地方,它能区分由相同的类表示的不同种类的表达式。其他的节点类型就不介绍了,有兴趣可以参考MSDN。

对于上面的例子,可以用下图描述生成的表达式树,值得注意的是,"叶子"表达式在代码中是最先创建的,,

表达式是自下而上构建的。表达式是不易变的,所有可以缓存和重用表达式。

将表达式编译成委托

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

Expression和Expression<TDelegate>的区别在于,泛型类以静态类型的方式标志了它是什么种类的表达式,也就是说,它确定了返回类型和参数例如上面的加法例子,返回值是一个int类型,没有参数,所以我们可以使用签名Func<int>与之匹配,所以可以用Expression<Func<int>>以静态类型的方式来表示该表达式

这样做的目的在于,LambdaExpression有一个Compile方法,该方法能创建一个恰当类型的委托。Expression<TDelegate>也有一个同名方法,该方法可以返回TDelegate类型的委托。获得了委托之后,我们就可以使用普通委托实例调用的方式来执行这个表达式。

接着上面加法的例子,我们把上面的加法表达式树转换成委托,然后执行委托:

Func<int> addDelegate = Expression.Lambda<Func<int>>(add).Compile();
Console.WriteLine(addDelegate());

从这个例子中我们看到怎么构建一个表达式树,然后把这个对象树编译成真正的代码。在.NET 3.5中的表达式树只能是单一的表达式,不能表示完整的类、方法。这在.NET 4.0中得到了一定的改进,表达式树可以支持动态类型,我们可以创建块,为表达式赋值等等。

将Lambda表达式转换为表达式树

Lambda表达式不仅可以创建委托实例,C# 3.0对于将Lambda表达式转换成表达式树提供了内建的支持。我们可以通过编译器把Lambda表达式转换成一个表达式树,并创建一个Expression<TDelegate>的一个实例。

下面的例子中我们将一个Lambda表达式转换成一个表达式树,并通过代码查看表达式树的各个部分:

static void Main(string[] args)
{    //将Lambda表达式转换为类型Expression<T>的表达式树    //expression不是可执行代码
    Expression<Func<int, int, int>> expression = (a, b) => a + b;

    Console.WriteLine(expression);    //获取Lambda表达式的主体
    BinaryExpression body = (BinaryExpression)expression.Body;
    Console.WriteLine(expression.Body);    //获取Lambda表达式的参数
    Console.WriteLine(" param1: {0}, param2: {1}", expression.Parameters[0], expression.Parameters[1]);
    ParameterExpression left = (ParameterExpression)body.Left;
    ParameterExpression right = (ParameterExpression)body.Right;
    Console.WriteLine(" left body of expression: {0}{4} NodeType: {1}{4} right body of expression: {2}{4} Type: {3}{4}", 
    left.Name, body.NodeType, right.Name, body.Type, Environment.NewLine);    //将表达式树转换成委托并执行
    Func<int, int, int> addDelegate = expression.Compile();
    Console.WriteLine(addDelegate(10, 16));
    Console.Read();
}

代码的输出为:

表达式树的用途

前面看到,通过Expression的派生类中的各种节点类型,我们可以构建表达式树;然后可以把表达式树转换成相应的委托类型实例,最后执行委托实例的代码。但是,我们不会绕这么大的弯子来执行委托实例的代码。

表达式树主要在LINQ to SQL中使用,我们需要将LINQ to SQL查询表达式(返回IQueryable类型)转换成表达式树。之所以需要转换是因为LINQ to SQL查询表达式不是在C#代码中执行的,LINQ to SQL查询表达式被转换成SQL,通过网络发送,最后在数据库服务器上执行。

这里只做个简单的介绍,后续会介绍LINQ to SQL相关的内容。

编译器对Lambda表达式的处理

前面我们了解到,Lambda可以用来创建委托实例,也可以用来生成表达式树,这些都是编译器帮我们完成的。

编译器如何决定生成可执行的IL还是一个表达式树:

  • 当Lambda表达式赋予一个委托类型的变量时,编译器生成与匿名方法同样的IL(可执行的委托实例)

  • 当Lambda表达式赋予一个Expression类型的变量时,编译器就将它转换成一个表达式树

下图展示了LINQ to Object和LINQ to SQL中Lambda表达式的不同处理方式:



--------------------- 

作者:hackpig

来源:www.skcircle.com

版权声明:本文为博主原创文章,转载请附上博文链接!


本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:
本帖最后由 勇哥,很想停止 于 2023-02-25 15:14:44 编辑

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

会员中心
搜索
«    2025年4月    »
123456
78910111213
14151617181920
21222324252627
282930
网站分类
标签列表
最新留言
    热门文章 | 热评文章 | 随机文章
文章归档
友情链接
  • 订阅本站的 RSS 2.0 新闻聚合
  • 扫描加本站机器视觉QQ群,验证答案为:halcon勇哥的机器视觉
  • 点击查阅微信群二维码
  • 扫描加勇哥的非标自动化群,验证答案:C#/C++/VB勇哥的非标自动化群
  • 扫描加站长微信:站长微信:abc496103864
  • 扫描加站长QQ:
  • 扫描赞赏本站:
  • 留言板:

Powered By Z-BlogPHP 1.7.2

Copyright Your skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864