2020-02-19

C# 委托 (一)—— 委托、 泛型委托与Lambda表达式

原创wnvalentin 最后发布于2018-08-19 20:46:47 阅读数 7421  收藏

展开

目录

1 委托的含义

2 委托声明、实例化和调用

2.1 委托的声明

2.2 委托的实例化

2.3 委托实例的调用

3 泛型委托

3.1 Func委托

3.2 Action委托

3.3 Predicate委托

4 匿名委托

5 Lambda表达式

5.1 表达式Lambda

5.2 语句Lambda

1 委托的含义

当需要将一个方法当作另一个方法的参数时,对于某些语言例如C/C++等,需要用函数指针来处理。而对于C#来说,则使用委托机制。

例如,当我们需要对一个泛型集合ICollection<T>进行排序时,我们定义一个Sort方法,那么这个方法需要哪些参数才能进行排序呢?首先,肯定需要一个Collection<T>对象作为输入参数,代表要排序的对象集合;然后,Sort方法还需要知道如何比较两个对象,经过比较之后才能决定让哪个对象排在前面。因此,Sort方法需要第二个参数,这个参数是一个方法,代表着比较排序对象的方法。这时候这个比较方法参数就只能是一个委托。我们在调用Sort方法对具体的类进行排序时,通过委托传入一个具体类的比较方法。

再比如,在使用LINQ时,我们经常会用到Where()、Find()等扩展方法,实现对集合中元素的筛选或查找。Where()方法拥有一个Func<T>参数,它就是一个委托。我们调用Where()时需要给这个委托传递一个方法,告诉Where筛选器筛选元素的规则方法。

委托是一种特殊的类,是一种能够引用方法的类。在创建委托时,就是创建了一个存储方法引用的对象。

委托是类型安全的。C函数指针只是一个指向一个存储单元的指针,不能保证指向的内容就是正确类型的函数。而对于C#的委托而言,声明一个委托时必须指定返回类型和参数,.NET编译器会严格检查方法的参数和返回类型和委托是否匹配,检查通过后才能进行转换。转换之后的委托实例作为一个参数,传递给调用它的函数。

一个委托可以被传递任何符合要求的方法。不同场合需要不同方法时,在调用的地方直接将委托参数替换为实际方法就行。因此,委托调用的方法是在程序运行时才能确定的。

2 委托声明、实例化和调用

2.1 委托的声明

前面提到过,委托是一种特殊的类,因此委托的声明与类的声明方法类似,在任何可以声明类的地方都可以声明委托。委托声明用delegate关键字,同时委托要指明方法参数和返回值,写法与方法类似。综合类的声明和方法的声明,委托声明写成如下形式:

[访问修饰符] delegate 返回值类型 委托名 (形参列表);

委托的声明实际上是定义了一个派生于System.Delegate类的类,这与一般类的声明语法不同。编译器会根据委托的声明自动创建一个委托的类并实现细节。

接下来我们以一个简单的List<Student>排序为例进行说明。假设我们有一个Student类,存放学生信息,拥有姓名、年龄和学号三个属性:

public class Student

{

    public string Name { get; set; }

    public int Age { get; set; }

    public int Num { get; set; }   

}

然后创建一个List<Student>:

Student s1 = new Student() { Name = "小红", Age = 10, Num = 1001 };

Student s2 = new Student() { Name = "小华", Age = 9, Num = 1002 };

List<Student> sList = new List<Student>();

sList.Add(s1);

List.Add(s2);

我们的目标是想给List<Student>对象添加一个排序方法,这个排序方法可以根据年龄或者学号来排序,具体需要哪一种排序需要在客户端调用时指定。(简单起见,本案例中List<Student>只包含两个元素,不纠结于排序算法)

按照要求,Student对象的比较有两种方法,我们实现两个比较方法,供委托使用:

//比较年龄

public static bool Younger(Student s1, Student s2) => s1.Age <= s2.Age;

//比较学号

public static bool NumSmaller(Student s1, Student s2) => s1.Num <= s2.Num;

由上,我们可以抽象出一个代表比较Student的方法的委托:

public delegate bool CompareDelegate(Student first, Student second);

这个委托的类名为CompareDelegate,注意到委托声明的返回值类型、参数与其代表的方法要完全一致。

2.2 委托的实例化

与普通类的使用方法相同,声明了委托之后,我们必须给委托传递一个具体的方法,才能在运行时调用委托实例。委托实例包含了被传递给它的方法的信息,在运行时,调用委托实例就相当于执行它当中的方法。

委托实例化格式如下:

委托类名 委托实例名 = new 委托类名(Target) ;

其中,委托实例名是自定义的名称,Target是要传入的方法的名称。注意,Target是方法的引用,不能带()。带()的话是该方法的调用。区分引用和调用。

委托的实例化还有一种简单的方法:

委托类名 委托实例名 = Target;

在需要委托实例的地方直接传入Target引用即可,C#编译器会自动根据委托类型进行验证,这称为“委托推断”。

案例:

//以下两种方法等价

CompareDelegate myCompareDelegate = new CompareDelegate(Younger);

CompareDelegate myCompareDelegate = Younger;//委托推断

2.3 委托实例的调用

委托实例等价于它当中实际方法,因此可以使用反射的Invoke()方法调用委托实例,也可以直接在委托实例后加上()进行调用。

我们下面看一下委托所代表的方法是如何被业务方法调用的。这里我们的业务是排序SortStudent方法:

//使用委托的业务方法

public static void SortStudent(List<Student> sList, CompareDelegate CompareMethod)

{

    if (CompareMethod(sList[0], sList[1]))//等价于CompareMethod.Invoke(sList[0],  List[1])

    {

        //sList[0]已经在sList[1]前面了,所以什么也不用做

    }

    else

    {

        sList.Reverse();//交换位置

    }

    //获取排名采用的比较方法的名称

    Console.WriteLine($"\r\n按照 {CompareMethod.Method.Name} 排名:");

    //打印排序后的链表

    foreach (Student s in sList)

        Console.WriteLine($"{s.Name} {s.Age} {s.Num} ");

}

这里Sort方法拥有一个CompareDelegate类型的委托实例CompareMethod,它可直接当做具体方法进行调用。

在客户端对委托进行实例化后,调用SortStudent()方法就可以进行排序了。

//委托的实例化与使用

CompareDelegate myCompareDelegate = NumSmaller;//采用比较学号的方法

SortStudent(sList, myCompareDelegate);

//使用委托推断,与上两行等价

SortStudent(sList, NumSmaller);

输出如下:

按照 NumSmaller 排名:

小红 10 1001

小华 9 1002

3 泛型委托

我们每次要使用一个委托时,都需要先声明这个委托类,规定参数和返回值类型,然后才能实例化、调用。为了简化这个过程, .NET 框架为我们封装了三个泛型委托类,因此大部分情况下我们不必再声明委托,可以拿来直接实例化使用,方便了我们的日常Coding。

.这三种泛型委托包括:Func<T>委托、Action<T>委托和Predicate<T>委托。

3.1 Func<T>委托

Func<T>委托代表着拥有返回值的泛型委托。Func<T>有一系列的重载,形式如 Func<T1,T2, ... TResult>,其中TResult代表委托的返回值类型,其余均是参数类型。只有一个T时,即Func<TResult>,代表该委托是无参数的。.NET封装了最多16个输入参数的Funct<>委托。

需要特别注意的是,若方法返回 void ,由于 void 不是数据类型,因此不能定义Func<void>委托。返回 void 的泛型委托见下文的Action<T>。

Func<T>的使用方法与一般的委托相同。例如上面的案例可改写如下;

public static void SortStudent(List<Student> sList,Func<Student,Student,bool> CompareMethod)

{

    if(CompareMethod(sList[0], sList[1]))

    {

    }

    else

    {

        sList.Reverse();

    }

    Console.WriteLine($"\r\n按照 {CompareMethod.Method.Name} 排名:");

    foreach (Student s in sList)

        Console.WriteLine($"{s.Name} {s.Age} {s.Num} ");

}

//客户端调用

Func<Student, Student, bool> myCompareFunc = NumSmaller;

SortStudent2(sList, myCompareFunc);

注意SortStudent2方法的委托参数也必须是Func<Student, Student, bool>,才能满足方法调用时类型一致的要求。 

3.2 Action<T>委托

Action<T>委托代表返回值为空 void 的委托,它也有一些列重载,最多拥有16个输入参数。用法与Func<T>相同。

3.3 Predicate<T>委托

这个一般用的较少,它封装返回值为bool类型的委托,可被Func<T>代替。

4 匿名委托

采用匿名方法实例化的委托称为匿名委托。

每次实例化一个委托时,都需要事先定义一个委托所要调用的方法。为了简化这个流程,C# 2.0开始提供匿名方法来实例化委托。这样,我们在实例化委托时就可以 “随用随写” 它的实例方法。

使用的格式是:

委托类名 委托实例名 = delegate (args) {方法体代码} ;

这样就可以直接把方法写在实例化代码中,不必在另一个地方定义方法。当然,匿名委托不适合需要采用多个方法的委托的定义。

使用匿名方法,以上代码可改写为:

CompareDelegate anonymousCompare = delegate (Student s3, Student s4)

{

    return s1.Num <= s2.Num;

};

SortStudent(sList, anonymousCompare);

需要说明的是,匿名方法并不是真的“没有名字”的,而是编译器为我们自动取一个名字。SortStudent方法打印了委托调用的方法的名字(见上文代码),我们可以看到如下输出:

按照 <Main>b__0 排名:

小红 10 1001

小华 9 1002

 编译器为我们的匿名方法取了一个b__0的名字。

5 Lambda表达式

江山代有才人出,纵然匿名方法使用很方便,可惜她很快就成了过气网红,没能领多长时间的风骚。如今已经很少见到了,因为delegate关键字限制了她用途的扩展。自从C# 3.0开始,她就被Lambda表达式取代,而且Lambda表达式用起来更简单。Lambda表达式本质上是改进的匿名方法。

Lambda表达式的灵感可能是来源于数学中的函数表达式,例如下图:


Lambda表达式把其中的箭头用 => 符号表示。

如今Lambda表达式已经应用在很多地方了,例如方法体表达式(Expression-Bodied Methods)、自动只读属性表达式等等。

Lambda表达式形式上分为两种:

5.1 表达式Lambda

当匿名函数只有一行代码时,可采用这种形式。例如:

CompareDelegate LambdaCompare = (s4, s5) => s4.Age <= s5.Age;

其中=>符号代表Lambda表达式,它的左侧是参数,右侧是要返回或执行的语句。参数要放在圆括号中,若只有一个参数,为了方便起见可省略圆括号。有多个参数或者没有参数时,不可省略圆括号。

相比匿名函数,在表达式Lambda中,方法体的花括号{}和return关键字被省略掉了。

其实,上文定义NumSmaller()和Younger()方法时,由于这两个方法主体只有一行代码,所以用的也是表达式Lambda,这是Lambda表达式的推广, 是C# 6 编译器提供的一个语法糖。

5.2 语句Lambda

当匿名函数有多行代码时,只能采用语句Lambda。例如,上面的表达式Lambda可改写为语句Lambda:

CompareDelegate LambdaCompare = (s4, s5) =>

{

    return s4.Age <= s5.Age;

};

语句Lambda不可以省略{}和return语句。

————————————————

版权声明:本文为CSDN博主「wnvalentin」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/wnvalentin/article/details/81840339

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容