.NET中那些所谓的新语法之三:系统预定义委托与Lambda表达式

开篇:在上一篇中,我们了解了匿名类、匿名方法与扩展方法等所谓的新语法,这一篇我们继续征程,看看系统预定义委托(Action/Func/Predicate)和超爱的Lambda表达式。为了方便码农们,.Net基类库针对实际开发中最常用的情形提供了几个预定义好的委托,这些委托可以直接使用,无需再重头定义一个自己的委托类型。预定义委托在.Net基类库中使用的比较广泛,比如在Lambda表达式和并行计算中都大量地使用,需要我们予以关注起来!

自 .NET Framework 3.5 (C# 3.0)以来,各种泛型委托纷涌而至,原先需要我们程序员手动定义的一些委托现在我们可以直接使用预定义的委托了,大大提高了开发效率,现在我们就首先来看看这些预定义的泛型委托。

一、无返回类型的内置委托—Action

1.1 初识Action

MSDN给出的定义:封装一个方法,该方法不具有参数并且不返回值

可以使用此委托以参数形式传递方法,而不用显式声明自定义的委托。封装的方法必须与此委托定义的方法签名相对应。也就是说,封装的方法不得具有参数,并且不得返回值。(在 C# 中,该方法必须返回 void)通常,这种方法用于执行某个操作。

  现在,我们来看看如何使用Action委托:

  (1)先看看之前我们是怎么来使用无返回值委托的例子:

public delegate void ShowValue();

public class Name
{
   private string instanceName;

   public Name(string name)
   {
      this.instanceName = name;
   }

   public void DisplayToConsole()
   {
      Console.WriteLine(this.instanceName);
   }

   public void DisplayToWindow()
   {
      MessageBox.Show(this.instanceName);
   }
}

public class testTestDelegate
{
   public static void Main()
   {
      Name testName = new Name("Koani");
      ShowValue showMethod = testName.DisplayToWindow;
      showMethod();
   }
}

  可以清楚地看出,我们之前要先显式声明了一个名为 ShowValue 的委托,并将对 Name.DisplayToWindow 实例方法的引用分配给其委托实例。

  (2)再看看有了Action委托之后我们怎么来达到上面的效果的例子:

public class Name
{
   private string instanceName;

   public Name(string name)
   {
      this.instanceName = name;
   }

   public void DisplayToConsole()
   {
      Console.WriteLine(this.instanceName);
   }

   public void DisplayToWindow()
   {
      MessageBox.Show(this.instanceName);
   }
}

public class testTestDelegate
{
   public static void Main()
   {
      Name testName = new Name("Koani");
      Action showMethod = testName.DisplayToWindow;
      showMethod();
   }
}

  可以清楚地看出,现在使用 Action 委托时,不必显式定义一个封装无参数过程的委托。

1.2 深入Action

  在实际开发中,我们经常将一个委托实例作为一个方法的参数进行传递,于是我们来看一下这个典型的场景,再通过Reflector反编译工具查看编译器到底帮我们做了什么好玩的事儿!

  (1)首先来看一下在List集合类型的ForEach方法的定义:

        //
        // 摘要:
        //     对 System.Collections.Generic.List<T> 的每个元素执行指定操作。
        //
        // 参数:
        //   action:
        //     要对 System.Collections.Generic.List<T> 的每个元素执行的 System.Action<T> 委托。
        //
        // 异常:
        //   System.ArgumentNullException:
        //     action 为 null。
        public void ForEach(Action<T> action);

  可以看出,ForEach方法的参数是一个Action委托实例,也就是说是一个无返回值的委托实例。

  (2)定义一个实体类,并通过Action委托使用ForEach方法:

    public class Person
    {
        public int ID { get; set; }

        public string Name { get; set; }

        public int Age { get; set; }
    }

    static void ActionDelegateDemo()
    {
         List<Person> personList = GetPersonList();

         personList.ForEach(new Action<Person>(delegate(Person p)
         {
                Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age);
          }));
     }

  可以看出,我们为ForEach方法传递了一个Action委托的实例,本质上是一个无返回值的方法指针,遍历输出了每个Person对象的信息。

  (3)也许有些童鞋看到上面的还是有点不解,只要你了解过委托,那么我们可以通过Reflector反编译工具去看看编译器到底做了啥事,Action委托的本质就会一如了然:(这里我们可以先看看没有Action的做法,是不是需要首先显式声明了一个无返回值的委托,然后是不是还要顶一个命名的无返回值的方法?)

  ①将编译好的程序集拖动到Reflector中,可以看到以下的情形:

  ②现在分别看看编译器为我们自动生成的无返回值的委托定义和方法定义:

  可以看出,不管是自动生成的委托还是方法,都是不带返回值的。

  ③有了上面的分析,我们再来看看执行的语句是怎么被编译的:

   可以看出,在编译后的代码里边连new Action<Person>()都省掉了,我们也可以知道,在代码中可以更加简化。但是,首先,我们得了解到底编译器是怎么识别Action委托的。于是,按照前两篇的思路,在反编译后的C#代码看不出什么端倪的时候,切换到IL代码一探究竟:

  由IL代码可以看出,还是原来的方法,还是原来的味道。委托还是那个委托,执行委托还是执行那个方法。这里,我们再来看看List类型的ForEach方法是怎么使用Action委托的:

  现在,我们可以知道,原来所不解的东西现在终于释怀了:在ForEach会通过一个循环遍历依次调用委托所持有的方法,这个方法是一个符合Action委托定义的无返回值方法。至于,为什么我们可以省略new Action<T>(),则是编译器为我们提供的一个便利。例如,我们在使用List<Person>对象的ForEach方法时,我们可以这样写:

personList.ForEach(delegate(Person p)
{
      Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age);
});

  首先,由于我们是使用的personList这个对象(List<Person>类型),所以编译器自动识别了泛型委托的T(即指定类型)为Person。其次,编译器自动将无返回值的匿名方法转换为了new Action<Person>对象。当然,如果是有返回值的匿名方法则会转换为指定类型的new Func<T>()对象,这里因为ForEach只接受无参数的委托实例或方法,所以如果传入了有返回值的匿名方法则会报错。

1.3 你究竟有几个Action可用?

  从图中可以看出,.NET Framework为我们提供了多达16个参数的Action委托定义,对于常见的开发场景已经完全够用了。

二、有返回类型的内置委托—Func

2.1 初识Func

MSDN给出的定义:封装一个具有一个参数并返回 TResult 参数指定的类型值的方法

  此委托的定义如下:

public delegate TResult Func<in T, out TResult>(T arg)

  (1)in T :此委托封装的方法的参数类型。

  (2)out TResult :此委托封装的方法的返回值类型。

可以使用此委托表示一种能以参数形式传递的方法,而不用显式声明自定义委托。封装的方法必须与此委托定义的方法签名相对应。也就是说,封装的方法必须具有一个通过值传递给它的参数,并且必须返回值。

  2.1.1 没有Func时的使用

delegate string ConvertMethod(string inString);

public class DelegateExample
{
   public static void Main()
   {
      ConvertMethod convertMeth = UppercaseString;
      string name = "Dakota";
      Console.WriteLine(convertMeth(name));
   }

   private static string UppercaseString(string inputString)
   {
      return inputString.ToUpper();
   }
}

  2.1.2 有了Func后的使用

public class GenericFunc
{
   public static void Main()
   {
      Func<string, string> convertMethod = UppercaseString;
      string name = "Dakota";

      Console.WriteLine(convertMethod(name));
   }

   private static string UppercaseString(string inputString)
   {
      return inputString.ToUpper();
   }
}

  当然,我们还可以借助匿名方法更加便捷地使用:

public class Anonymous
{
   public static void Main()
   {
      Func<string, string> convert = delegate(string s)
         { return s.ToUpper();}; 

      string name = "Dakota";
      Console.WriteLine(convert(name));   
   }
}

  可以清楚地看出,现在使用 Func 委托时,不必显式定义一个新委托并将命名方法分配给该委托。

2.2 深入Func

  2.2.1 用法先行:爽一下

  我们已经知道Func委托是带指定返回值类型的委托,那么我们来看看在实际开发场景的一幕。还是以刚刚那个数据集合PersonList为例,在很多时候我们需要对从数据库中读取的数据集合进行二次筛选,这时我们可以使用List集合的Select方法,我们将一个Func委托实例作为方法参数传递给Select方法,就可以返回一个符合我们指定条件的新数据集合。

  (1)先来看看Select方法的定义:

        //
        // 摘要:
        //     将序列中的每个元素投影到新表中。
        //
        // 参数:
        //   source:
        //     一个值序列,要对该序列调用转换函数。
        //
        //   selector:
        //     应用于每个元素的转换函数。
        //
        // 类型参数:
        //   TSource:
        //     source 中的元素的类型。
        //
        //   TResult:
        //     selector 返回的值的类型。
        //
        // 返回结果:
        //     一个 System.Collections.Generic.IEnumerable<T>,其元素为对 source 的每个元素调用转换函数的结果。
        //
        // 异常:
        //   System.ArgumentNullException:
        //     source 或 selector 为 null。
        public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);

  可以看出,Select方法中的参数采用了Func泛型委托,根据泛型委托的定义TSource和TResult分别代表要传入的数据类型以及要返回的数据类型。

  (2)再来看看如何在程序中使用Func委托:

  首先定义一个与源数据类型不同的新数据类型作为返回值类型:

    public class LitePerson
    {
        public string Name { get; set; }
    }

  ①标准定义版:

            List<Person> personList = GetPersonList();
            
            IEnumerable<LitePerson> litePersonList = personList.Select<Person, LitePerson>(
                new Func<Person, LitePerson>
                (
                    delegate(Person p)
                    {
                        return new LitePerson() { Name = p.Name };
                    }
                )
            );    

  ②嘻哈简化版:借助编译器提供的自动识别,简化我们的代码

            IEnumerable<LitePerson> litePersonList = personList.Select(
                delegate(Person p)
                {
                    return new LitePerson() { Name = p.Name };
                }
            );

  ③绝逼懒人版:借助匿名类和泛型可以大大简化我们的代码

            var liteList = personList.Select(delegate(Person p)
            {
                return new { Name = p.Name, AddDate = DateTime.Now };
            });

  (3)调试运行可以得到以下结果:

  2.2.2 原理为王:探一次

  (1)通过Reflector反编译,我们再来看看编译器帮我们生成的东东:

  (2)看看自动生成的委托和方法的定义:

  相信经过上节Action的详细分析,这里大家应该也可以举一反三了解编译器帮我们到底做了什么事儿了,这里我就不再赘述了,后面也不会再赘述此方面的东东(为了节省页面大小)。

  当然,和Action类似,.NET基类库为我们也提供了多达16个输入参数的Func委托,但是,输出参数却只有1个。

三、返回bool类型的内置委托—Predicate

3.1 初识Predicate

  经过了Func的了解,我们可以知道接下来的这两个Predicate和Comparison其实都属于有返回值类型的委托,他们不过是两个具体的特殊实例而已(一个返回bool类型,一个返回int类型)。

MSDN给出的定义:表示定义一组条件并确定指定对象是否符合这些条件的方法

  它的定义很简单:(这里就不再对其进行解释了)

public delegate bool Predicate<in T>(T obj)

此委托由 Array 和 List<T> 类的几种方法使用,常用于在集合中搜索元素。

3.2 深入Predicate

  由于Predicate委托常用于在集合中搜索元素,那么我们就来看看如何使用Predicate委托来进行元素的搜索。于是,我们将目光转到List集合的FindAll方法,相信大部分童鞋都用过这个方法。

  (1)先来看看FindAll的定义:

        //
        // 摘要:
        //     检索与指定谓词定义的条件匹配的所有元素。
        //
        // 参数:
        //   match:
        //     System.Predicate<T> 委托,用于定义要搜索的元素应满足的条件。
        //
        // 返回结果:
        //     如果找到,则为一个 System.Collections.Generic.List<T>,其中包含与指定谓词所定义的条件相匹配的所有元素;否则为一个空
        //     System.Collections.Generic.List<T>。
        //
        // 异常:
        //   System.ArgumentNullException:
        //     match 为 null。
        public List<T> FindAll(Predicate<T> match);

  (2)再来看看FindAll的实现:

  (3)现在我们来用一下Predicate委托:还是以那个PersonList集合为例,假如我们要筛选出Age>20的Person,我们就可以使用FindAll方法。现在我们来写一下这个委托:(后面我们会用Lambda表达式来简写,那才叫一个爽!)可以看出,关键点在于:delegate(Person p) { return p.Age > 20; }这一句上,传入参数是Person类型的对象,返回的是一个比较结果即bool值。

            List<Person> personList = GetPersonList();

            List<Person> agedList = personList.FindAll(
                new Predicate<Person>(delegate(Person p) 
                    { 
                        return p.Age > 20; 
                    }
                )
            );

四、返回int类型的内置委托—Comparison

4.1 初识Comparison

MSDN给出的定义:表示比较同一类型的两个对象的方法

  它的定义也很简单:

public delegate int Comparison<in T>(T x, T y)

  T是要比较的对象的类型,而返回值是一个有符号整数,指示 x 与 y 的相对值,如下表所示:

含义

小于 0

x 小于 y。

0

x 等于 y。

大于 0

x 大于 y。

此委托由 Array 类的 Sort<T>(T[], Comparison<T>) 方法重载和 List<T> 类的 Sort(Comparison<T>) 方法重载使用,用于对数组或列表中的元素进行排序

4.2 深入Comparison

  由于Comparison委托常用于在集合中进行排序,那么我们就来看看如何使用Comparison委托来进行元素的排序。于是,我们将目光转到List集合的Sort方法,相信大部分童鞋也都用过这个方法。

  (1)老惯例,还是先看看Sort方法的定义:

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

  (2)再来看看Sort方法的实现:

  可以看出,这里虽然使用Comparison委托但最终还是转换成了Comparer比较器,再次调用重载的Array.Sort静态方法进行排序。

  (3)现在我们来用一下Comparison委托:还是以那个PersonList集合为例,假如我们要以Age为条件进行降序排列,我们应该怎么来写这个委托呢?

List<Person> personList = GetPersonList();

personList.Sort(delegate(Person p1, Person p2)
{
      return p2.Age - p1.Age;
});

personList.ForEach(delegate(Person p)
{
      Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age);
});

  实现的效果如下图所示:

  那么,如果是要进行升序排列呢?只需要改一下:return p2.Age-p1.Age; 更改一下被减数和减数的位置,即可完成升序和降序的切换。

personList.Sort(delegate(Person p1, Person p2)
{
      return p1.Age - p2.Age;
});

五、Lambda表达式:[ C# 3.0/.NET 3.x 新增特性 ]

  回顾,发现上面的代码,需要传一个 匿名方法 ,写起来特别别扭。于是我们很想知道能否有简化的语法呢?微软告诉咱们:Of Course,必须有,它就是Lambda表达式。Lambda表达式是比匿名方法更简洁的一种匿名方法语法。

Lambda来源:1920年到1930年期间,数学家Alonzo Church等人发明了Lambda积分。Lambda积分是用于表示函数的一套系统,它使用希腊字母Lambda(λ)来表示无名函数。近年来,函数式编程语言(如Lisp)使用这个术语来表示可以直接描述函数定义的表达式,表达式不再需要有名字了。

5.1 初识Lambda表达式

  5.1.1 Lambda表达式要点

    ①Lambda表达式中的参数列表(参数数量、类型和位置)必须与委托相匹配

    ②表达式中的参数列表不一定需要包含类型,除非委托有ref或out关键字(此时必须显示声明);

    ③如果没有参数,必须使用一组空的圆括号

  5.1.2 Lambda使用示例

        static void LambdaDemo()
        {
            List<Person> personList = GetPersonList();
            Console.WriteLine("--------------------标准预定义委托--------------------");
            Console.WriteLine("Standard Action Delegate Show:");
            personList.ForEach(new Action<Person>(delegate(Person p)
                {
                    Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age);
                })
            );

            Console.WriteLine("Simple Action Delegate Show:");
            personList.ForEach(delegate(Person p)
            {
                Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age);
            });
            Console.WriteLine("--------------------Lambda表达式--------------------");
            Console.WriteLine("Lambda Expression Show:");
            personList.ForEach(p =>
                Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age));

            Console.WriteLine("FindAll:");
            var dataList = personList.FindAll(p => p.Age > 20);
            foreach (var item in dataList)
            {
                Console.WriteLine(item.ID + "-" + item.Name + "-" + item.Age);
            }

            Console.WriteLine("Sort:");
            personList.Sort((p1, p2) => p1.Age - p2.Age);

            Console.WriteLine("Select:");
            var selectList = personList.Select(p => new LitePerson() { Name = p.Name });
            foreach(var item in selectList)
            {
                Console.WriteLine(item.Name);
            }
        }

   调试运行的结果如下:

  5.1.3 Lambda本质探析

  (1)以上述案例中的Sort方法为例:personList.Sort((p1, p2) => p1.Age - p2.Age);

(2)通过反编译工具,可以看到其实是声明了一个Comparison委托实例:

  (3)现在,我们来分析一下具体的步凑:有了前面的基础,现在再来看就轻松了许多,So Easy!

    ①编译器自动生成了一个Comparison委托:

    ②编译器帮我们创建了一个符合Comparison委托签名的静态方法:

    ③实例化Comparison委托变量,并将方法指针传入该委托;

    ④调用List<T>实例的Sort方法,并传入Comparison委托实例;

    其中,前面两步①和②可以通过反编译后的C#代码获知,而后面两步③和④则需要通过IL代码来分析,前面已经介绍过相关,这里就不再赘述。

5.2 回顾Lambda进化史

  前面了解了Lambda是什么,这里我们来回顾一下Lambda的演化过程。

  从演化过程可以知道,编译器在越来越智能地帮我们做着更多的事儿,而我们却在享受着编译器带来的便利沉浸在高效的开发效率中,变得越来越“懒”了。

5.3 语句Lambda

  Lambda表达式有两种类型:一是Lambda表达式,二是语句Lambda。

  那么,语句Lambda和表达式Lambda到底有何区别?

ANSWER:语句Lambda 和 表达式Lambda 的区别在于,前者在 =>右边有一个语句块(大括号),而后者只有一个表达式(没有return 和大括号)。 EXAMPLES: (1)表达式Lambda: list.FindAll(d => d.Id > 2);// goes to list.ForEach(d => Response.Write(d.ToString() + "<br/>")); (2)语句Lambda: list.ForEach(d => { if (d.Id > 2) { Response.Write(d.ToString() + "<br/>"); } });   可以看出,语句Lambda的右侧有一个语句块,在这个大括号内的语句可能会有多条。

参考文章

  (1)金旭亮,《C#面向对象程序设计》,教案6-委托与事件讲义:http://download.csdn.net/detail/bitfan/3324733

  (2)MSDN,泛型委托(C#编程指南):http://msdn.microsoft.com/zh-cn/library/sx2bwtw7.aspx

  (3)min,《泛型委托在项目中的应用》:http://www.cnblogs.com/ASPNET2008/archive/2010/04/05/1704405.html

  (4)MSDN,Lambda表达式(C#编程指南):http://msdn.microsoft.com/zh-cn/library/bb397687.aspx

  (5)张龙豪,《Lambda表达式详解》:http://www.cnblogs.com/knowledgesea/p/3163725.html

附件下载

  NewGrammerDemos v1.2:http://pan.baidu.com/s/1gdxi39D

作者:周旭龙

出处:http://www.cnblogs.com/edisonchou/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏hbbliyong

LINQ查询操作符 LINQ学习第二篇

一、投影操作符 1. Select Select操作符对单个序列或集合中的值进行投影。下面的示例中使用select从序列中返回Employee表的所有列: ...

3685
来自专栏知识分享

C#中public与private与static

现在静下心来想要重新细致的过一遍C#,因为自己做C#没有底气,, 闲话少说 先来一句话 public(共有的) 声明的方法和属性,可以被外部调用. privat...

3034
来自专栏cs

c#知识点1.0数据类型

以前就说,要开始写c#的博客,最近把linux大约写完了,现在开始c#了,java的博客简书一大堆,我就避免撞车吧,其实我是菜鸟(嘻嘻,写不出更好的了) 数据...

3847
来自专栏我是业余自学C/C++的

字典 原

字典(dictionary)是由一些形如(key,value)的数对所组成的集合,其中key是关键字,value是与关键字key对应的值(另一种说法是,valu...

1301
来自专栏机器学习入门

LWC 61:738. Monotone Increasing Digits

LWC 61:738. Monotone Increasing Digits 传送门:738. Monotone Increasing Digits Probl...

2097
来自专栏博客园

.NET面试题解析(03)-string与字符串操作

4.以下代码执行后内存中会存在多少个字符串?分别是什么?输出结果是什么?为什么呢?

922
来自专栏GreenLeaves

C# 委托的一些使用上的小技巧

1、委托是一种数据类型,我们可以在任何定义类的地方定义委托,在任何声明类的地方声明委托 2、初始化委托有两种方式,代码如下: (1)、像类一样初始化委托 pub...

2076
来自专栏C# 编程

[C#]使用Join与GroupJoin将两个集合进行关联与分组

本文为原创文章、源代码为原创代码,如转载/复制,请在网页/代码处明显位置标明原文名称、作者及网址,谢谢! 本文使用的开发环境是VS2017及dotNet4.0,...

2490
来自专栏C/C++基础

(int),Int32.Parse,Convert.ToInt3…

(int)是一种被称为强制转换的显示转换。源变量和目标变量必须是兼容的(必须都是int类型的)。并且有丢失数据的风险。因为目标变量的类型大小小于源变量。

1163
来自专栏菩提树下的杨过

数据结构C#版笔记--单链表(LinkList)

上一篇学习了"顺序表(SeqList)",这一篇来看下“单链表(LinkList)”。在上一篇的最后,我们指出了:顺序表要求开辟一组连续的内存空间,而且插入/删...

2587

扫码关注云+社区

领取腾讯云代金券