C# Note2:委托(delegate) & Lambda表达式 & 事件(event)
前言
本文主要讲述委托和Lambda表达式的基础知识,以及如何通过Lambda表达式实现委托调用,并阐述.NET如何将委托用作实现事件的方式。
参考:C#高级编程
1.什么是委托(delegate)?
delegate是C#中的一种类型,它是一个能够持有对某个方法的引用的类。与其它类不同的是,delegate类能够拥有一个签名(signature),并且它"只能持有与其签名相匹配的方法的引用"。委托可以看成寻址方法的.NET版本(可对比C++中的函数指针进行理解),你可以传递类A的方法f给类B的对象,使得类B的对象能够调用这个方法f。不过,函数指针只是一个指向内存位置的指针(无法判断指针的实际指向,也无从知晓参数和返回类型等),不是类型安全的,而delegate是面向对象、类型安全、可靠的受控(managed)对象(即运行时能保证delegate指向一个有效的方法,无须担心它会指向无效地址或者越界地址)。
2.为什么要使用委托?
使用委托使程序员可以将方法引用封装在委托对象内。然后可以将该委托对象传递给可调用所引用方法的代码,而不必在编译时知道将调用哪个方法。与C或C++中的函数指针不同,委托是面向对象,而且是类型安全的。
2.1用的时机
当要把方法传递给其他方法时,需要使用委托。我们习惯于把数据作为参数传递给方法,如 int i = int.Parse("99");而有时某个方法执行的操作并不是针对数据进行,而是要对另外一个方法进行操作。在编译时,我们不知道第二个方法是什么,其信息只能在运行时得到。很明显的示例有:
- 启动线程和任务:在计算机并行运行某些新的执行序列的同时运行当前的任务,这样的序列就叫做线程。在其中一个基类System.Threading,Thread的一个实例上使用方法Start(),就可以启动一个线程。在告诉计算机启动一个新的执行序列时,必须为其提供开始启动的方法的细节,即Thread类的构造函数必须带有一个参数,该参数定义了线程调用的方法。
- 通用库类:许多库包含执行各种标准任务的代码。例如,需要编写一个类,它带有一个对象数组,将它们按照升序排列。但是,排序的部分过程会涉及到重复使用数组的两个对象进行比较。若要编写的类能够对任何对象进行排序,就无法提前告诉计算机应该如何比较对象。处理类中对象数组的客户端代码必须给类传递某个可以调用并进行这种比较的合适方法的细节。
在C/C++中,只能提取函数的地址,并作为一个参数传递它。这种直接方法不仅会导致一些关于类型安全性的问题,且没有意思到:在进行面向对象编程时,几乎没有方法是孤立存在的,在调用方法前通常需要与类实例相关联。.NET Framwork在语法上不允许使用这种直接方法。如果要传递方法,必须将方法的细节封装在一种新类型的对象中,即委托。委托只是一种特殊类型的对象,其特殊点在于一般的对象都包含数据,而它包含的只是一个或多个方法的地址。
2.2声明委托
C#中使用一个类的两个阶段:(1)定义类,告诉编译器该类由什么字段和方法组成;(2)(除非只使用静态方法)实例化类的一个对象。
委托也要经过这两个步骤。定义委托的语法:delegate void IntMethodInvoker(int x); (定义时必须给出它所表示的方法的签名和返回类型等全部细节,以保证高的类型安全性)
它表示的方法可以不带参数。因为定义委托基本上是定义一个新类,所以可以在定义类的任何相同地方定义委托(可以是任何类的内部或外部,还可以在名称空间中把委托定义为顶层对象)。
同样在其定义上,可以应用常见的访问修饰符:public、private、protected等。
2.3使用委托
private delegate string GetAStringent(); static void Main()
{
int x = 40;
//x.ToString后面不能加(),否则就会返回一个不能赋予委托变量的字符串对象。只能将方法的地址赋予委托变量
GetAString firstStringMethod = new GetAString(x.ToString); //实例化类型为GetAString 的一个委托,并对它进行初始化,使它引用整型变量x的ToString()方法
Console.WriteLine("String is {0}", firstStringMethod());
//the above statement is equivalent to saying
//Console.WriteLine("String is {0}", x.ToString());
}
给委托实例提供圆括号与调用委托类的Invoke()方法完全相同。即firstStringMethod()可以替换为firstStringMethod.Invoke()。
为了减少输入量,只要需要委托实例,就可以只传送地址的名称。这叫委托推断。
委托推断可以在需要委托实例的任何地方使用,它也可用于事件,因为事件基于委托(后面会讲到)
为了做此说明,对上面的代码进行扩展:
使用firstStringMethod委托在另一个对象上调用其它两个方法,一个是实例方法,一个是静态方法。
namespace Wrox.ProCSharp.Delegates
{
struct Currency
{
public uint Dollars;
public ushort Cents; public Currency(uint dollars, ushort cents)
{
this.Dollars = dollars;
this.Cents = cents;
} public override string ToString()
{
return string.Format("${0}.{1,-2:00}", Dollars, Cents);
} public static string GetCurrencyUnit()
{
return "Dollar";
} public static explicit operator Currency(float value)
{
//explicit关键字的作用是强制转换用户自定义的类型转换运算符.通常前面用static后面用operator,一般是把当前类型转换成另一个类型(将原类型的转换成目标类型)
checked
{
uint dollars = (uint)value;
ushort cents = (ushort)((value - dollars) * 100);
return new Currency(dollars, cents);
}
} public static implicit operator float(Currency value)
{
//implicit关键字用于声明隐式的用户定义类型转换运算符。 如果可以确保转换过程不会造成数据丢失,则可使用该关键字在用户定义类型和其他类型之间进行隐式转换
return value.Dollars + (value.Cents / 100.0f);
} public static implicit operator Currency(uint value)
{
return new Currency(value, 0);
} public static implicit operator uint(Currency value)
{
return value.Dollars;
}
} }
下面使用GetAString实例:
using System; namespace Wrox.ProCSharp.Delegates
{
class Program
{
private delegate string GetAString(); static void Main()
{
int x = 40;
GetAString firstStringMethod = x.ToString;
Console.WriteLine("String is {0}", firstStringMethod()); Currency balance = new Currency(34, 50); // firstStringMethod references an instance method
firstStringMethod = balance.ToString;
Console.WriteLine("String is {0}", firstStringMethod()); // firstStringMethod references a static method
firstStringMethod = new GetAString(Currency.GetCurrencyUnit);
Console.WriteLine("String is {0}", firstStringMethod()); }
}
}
运行结果:
上面的例子,实际上并未说明委托的本质,或者说将一个委托传递给另一个方法的具体过程。下面继续举例说明,没有委托就难完成的工作:
namespace Wrox.ProCSharp.Delegates
{
class MathOperations
{
public static double MultiplyByTwo(double value)
{
return value * 2;
} public static double Square(double value)
{
return value * value;
}
} }
using System; namespace Wrox.ProCSharp.Delegates
{
delegate double DoubleOp(double x); class Program
{
static void Main()
{
//实例化了一个委托数组DoubleOp
DoubleOp[] operations =
{
MathOperations.MultiplyByTwo,
MathOperations.Square
}; for (int i = 0; i < operations.Length; i++)
{
Console.WriteLine("Using operations[{0}]:", i);
ProcessAndDisplayNumber(operations[i], 2.0);
ProcessAndDisplayNumber(operations[i], 7.94);
ProcessAndDisplayNumber(operations[i], 1.414);
Console.WriteLine();
}
} static void ProcessAndDisplayNumber(DoubleOp action, double value)
{
double result = action(value); //调用action委托实例封装的方法,返回结果存储在result中
Console.WriteLine(
"Value is {0}, result of operation is {1}", value, result);
}
} }
上例说明了使用委托的一种方式:把方法组合到一个数组中,就可以循环调用不同的方法。
- BubbleSorter示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; namespace Wrox.ProCSharp.Delegates
{
class BubbleSorter
{
static public void Sort<T>(IList<T> sortArray, Func<T, T, bool> comparison)
{
//冒泡排序的思想:重复遍历数组,比较每一对数字,交换位置,从而把最大或者最小的数字逐步移动到数组的最后!
bool swapped = true;
do
{
swapped = false;
for (int i = 0; i < sortArray.Count - 1; i++)
{
if (comparison(sortArray[i+1], sortArray[i]))
{
T temp = sortArray[i];
sortArray[i] = sortArray[i + 1];
sortArray[i + 1] = temp;
swapped = true;
}
}
} while (swapped); }
} }
其中,使用委托Func<T1, T2, TResult>传递一个封装的方法,用于比较两个新类的大小。
需要定义另一个类,建立起要排序的数组。这里构造一个员工列表,根据他们的薪水进行排序:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; namespace Wrox.ProCSharp.Delegates
{
class Employee
{
public Employee(string name, decimal salary)
{
this.Name = name;
this.Salary = salary;
} public string Name { get; private set; }
public decimal Salary { get; private set; } public override string ToString()
{
return string.Format("{0}, {1:C}", Name, Salary);
} public static bool CompareSalary(Employee e1, Employee e2)
{
return e1.Salary < e2.Salary;
}
} }
为了匹配Func<T, T, bool>委托的签名,在该类中必须定义CompareSalary,它的参数是两个Employee引用,并返回一个布尔值。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; namespace Wrox.ProCSharp.Delegates
{
class Program
{
static void Main()
{
Employee[] employees =
{
new Employee("Bugs Bunny", 20000),
new Employee("Elmer Fudd", 10000),
new Employee("Daffy Duck", 25000),
new Employee("Wile Coyote", 1000000.38m),
new Employee("Foghorn Leghorn", 23000),
new Employee("RoadRunner", 50000)
}; BubbleSorter.Sort(employees, Employee.CompareSalary); foreach (var employee in employees)
{
Console.WriteLine(employee);
} }
}
}
- 多播委托
前面的每个委托都只包含了一个方法调用。如果要调用多个方法,则需要多次显式调用这个委托。委托也可以包含多个方法,称为多播委托。
using System; namespace Wrox.ProCSharp.Delegates
{
class MathOperations
{
public static void MultiplyByTwo(double value)
{
double result = value * 2;
Console.WriteLine("Multiplying by 2: {0} gives {1}", value, result);
} public static void Square(double value)
{
double result = value * value;
Console.WriteLine("Squaring: {0} gives {1}", value, result);
}
} }
using System; namespace Wrox.ProCSharp.Delegates
{
class Program
{
static void Main()
{
Action<double> operations = MathOperations.MultiplyByTwo;
operations += MathOperations.Square; ProcessAndDisplayNumber(operations, 2.0);
ProcessAndDisplayNumber(operations, 7.94);
ProcessAndDisplayNumber(operations, 1.414);
Console.WriteLine();
} static void ProcessAndDisplayNumber(Action<double> action, double value)
{
Console.WriteLine();
Console.WriteLine("ProcessAndDisplayNumber called with value = {0}", value);
action(value); }
} }
通过多播委托调用多个方法可能导致一个大问题:多播委托包含一个逐个调用的委托集合,如果通过委托调用其中一个方法抛出异常,整个迭代就会停止。
另外,如果我们要获得委托集合所有的返回值,可以使用GetInvocationList方法(Returns the invocation list of this multicast delegate, in invocation order.)。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; namespace Wrox.ProCSharp.Delegates
{
class Program
{
static void One()
{
Console.WriteLine("One");
throw new Exception("Error in one");
} static void Two()
{
Console.WriteLine("Two");
} static void Main()
{
Action d1 = One;//Action:Encapsulates a method that has no parameters and does not return a value.
d1 += Two; Delegate[] delegates = d1.GetInvocationList();
foreach (Action d in delegates)
{
try
{
d();
}
catch (Exception)
{
Console.WriteLine("Exception caught");
}
} }
}
}
3.Lambda表达式与委托的关系?
两者是直接相关的,当参数是委托类型时,就可以使用Lambda表达式实现委托引用的方法。
using System; namespace Wrox.ProCSharp.Delegates
{
class Program
{
static void Main()
{
SimpleDemos(); int someVal = 5;
Func<int, int> f = x => x + someVal; someVal = 7; Console.WriteLine(f(3));
} static void SimpleDemos()
{
Func<string, string> oneParam = s => String.Format("change uppercase {0}", s.ToUpper());
Console.WriteLine(oneParam("test")); Func<double, double, double> twoParams = (x, y) => x * y;
Console.WriteLine(twoParams(3, 2)); Func<double, double, double> twoParamsWithTypes = (double x, double y) => x * y;
Console.WriteLine(twoParamsWithTypes(4, 2)); Func<double, double> operations = x => x * 2;
operations += x => x * x; ProcessAndDisplayNumber(operations, 2.0);
ProcessAndDisplayNumber(operations, 7.94);
ProcessAndDisplayNumber(operations, 1.414);
Console.WriteLine();
} static void ProcessAndDisplayNumber(Func<double, double> action, double value)
{
double result = action(value);
Console.WriteLine(
"Value is {0}, result of operation is {1}", value, result);
}
}
}
4.委托和事件的关系及区别?
事件基于委托,为其提供了一种发布/订阅机制,换句话说事件是特殊类型的多路广播委托,仅可从声明它们的类或结构(发布者类)中调用。如果其他类或结构订阅了该事件,则当发布者类引发该事件时,会调用其事件处理程序方法。
不过,event在delegate的基础上作了两点限制:
- 外部类只能看到和使用委托所提供的+=和-=行为,不能直接对其赋值(即=操作),即使继承类也是如此,这样可以不影响委托对其他观察者的通知.(委托的invoke或GetInvocationList等方法在event中不能使用)
- 只有声明类可以调用(或触发)一个事件,外部类不可以直接调用其事件。
思考:如何方便地移除事件的订阅?
参考:通过反射实现
4.1事件发布程序
示例:事件用于连接CarDealer类和Consumer类。CarDealer类提供一个新车到达时触发的事件。Consumer类订阅该事件,以获得新车到达的通知。
using System; namespace Wrox.ProCSharp.Delegates
{
public class CarInfoEventArgs : EventArgs
{
public CarInfoEventArgs(string car)
{
this.Car = car;
} public string Car { get; private set; }
} public class CarDealer
{
public event EventHandler<CarInfoEventArgs> NewCarInfo; public void NewCar(string car)
{
Console.WriteLine("CarDealer, new car {0}", car);
if (NewCarInfo != null)
{
NewCarInfo(this, new CarInfoEventArgs(car));
}
}
}
}
public event EventHandler<CarInfoEventArgs> NewCarInfo; 定义事件是C#的简化记法。编译器会创建一个EventHandler<CarInfoEventArgs>委托类型的变量,并添加方法,以便从委托中订阅和取消。该简化记法的较长形式如下所示:
public delegate EventHandler<CarInfoEventArgs> NewCarInfo;
public event EventHandler<CarInfoEventArgs> NewCarInfo
{
add
{
newCarInfo += value;
}
remove
{
newCarInfo = value;
}
}
4.2事件侦听器
using System; namespace Wrox.ProCSharp.Delegates
{
public class Consumer
{
private string name; public Consumer(string name)
{
this.name = name;
} public void NewCarIsHere(object sender, CarInfoEventArgs e)
{
Console.WriteLine("{0}: car {1} is new", name, e.Car);
}
}
}
连接事件发布程序和订阅器:使用CarDealer类的NewCarInfo事件,通过“+=”创建一个订阅。通过“-=”取消订阅。
namespace Wrox.ProCSharp.Delegates
{
class Program
{
static void Main()
{
var dealer = new CarDealer(); var michael = new Consumer("Michael");
dealer.NewCarInfo += michael.NewCarIsHere; //消费者michael(变量)订阅了事件 dealer.NewCar("Mercedes"); //一辆Mercedes到达,Michael得到了通知 var nick = new Consumer("Nick");
dealer.NewCarInfo += nick.NewCarIsHere; dealer.NewCar("Ferrari"); dealer.NewCarInfo -= michael.NewCarIsHere; dealer.NewCar("Toyota");
}
}
}
4.3弱事件
通过事件,直接连接到发布程序和侦听器,这样垃圾回收会存在问题:如果侦听器不再直接引用,发布程序仍有一个引用。垃圾回收器不能清空侦听器的内存,因为发布程序仍保有一个引用,会针对侦听器触发事件。
这种强连接可以通过弱事件模式来解决,即使用WeekEventManager作为发布程序和侦听器之间的中介。
- 弱事件管理器
using System.Windows; namespace Wrox.ProCSharp.Delegates
{
public class WeakCarInfoEventManager : WeakEventManager
{
public static void AddListener(object source, IWeakEventListener listener)
{
CurrentManager.ProtectedAddListener(source, listener);
} public static void RemoveListener(object source, IWeakEventListener listener)
{
CurrentManager.ProtectedRemoveListener(source, listener);
} public static WeakCarInfoEventManager CurrentManager //静态属性CurrentManager创建了一个WeakCarInfoEventManager类型的对象(如果它不存在),并返回对该对象的引用
{
get
{
WeakCarInfoEventManager manager = GetCurrentManager(typeof(WeakCarInfoEventManager)) as WeakCarInfoEventManager;
if (manager == null)
{
manager = new WeakCarInfoEventManager();
SetCurrentManager(typeof(WeakCarInfoEventManager), manager);
}
return manager;
}
} protected override void StartListening(object source) //重写:添加第一个侦听器时调用该方法
{
(source as CarDealer).NewCarInfo += CarDealer_NewCarInfo;
} void CarDealer_NewCarInfo(object sender, CarInfoEventArgs e)
{
DeliverEvent(sender, e); //把事件传递给侦听器
}
protected override void StopListening(object source) //重写:删除最后一个侦听器时调用该方法
{
(source as CarDealer).NewCarInfo -= CarDealer_NewCarInfo;
}
}
}
- 事件侦听器
using System;
using System.Windows; namespace Wrox.ProCSharp.Delegates
{
public class Consumer : IWeakEventListener //实现IWeakEventListener接口
{
private string name; public Consumer(string name)
{
this.name = name;
} public void NewCarIsHere(object sender, CarInfoEventArgs e)
{
Console.WriteLine("{0}: car {1} is new", name, e.Car);
} bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) //触发事件时,从弱事件管理器中调用IWeakEventListener定义的ReceiveWeakEvent方法
{
NewCarIsHere(sender, e as CarInfoEventArgs);
return true;
} }
}
Main()方法:
namespace Wrox.ProCSharp.Delegates
{
class Program
{
static void Main()
{
var dealer = new CarDealer(); var michael = new Consumer("Michael");
WeakCarInfoEventManager.AddListener(dealer, michael); dealer.NewCar("Mercedes"); var nick = new Consumer("Nick");
WeakCarInfoEventManager.AddListener(dealer, nick); dealer.NewCar("Ferrari"); WeakCarInfoEventManager.RemoveListener(dealer, michael); dealer.NewCar("Toyota");
}
}
}
实现了弱事件模式后,发布程序和侦听器就不再强连接了。当不再引用侦听器时,他就会被垃圾回收。
C# Note2:委托(delegate) & Lambda表达式 & 事件(event)的更多相关文章
- 委托/lambda表达式/事件
委托 委托是执行安全的类,它的使用方式与类类似(即都需要定义再实例化),不同在于,类在实例化之后叫对象或类的实例,但委托在实例化后仍叫委托,委托可以把函数作为参数传递. 语法声明: delegate ...
- 委托学习过程及委托、Lambda表达式和匿名方法的关系总结及事件总结
第一章,当开始学习委托的时候,我们会问什么是委托?为什么要学习委托? 一,什么是委托? 委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法, ...
- C#高级编程(第9版) 第08章 委托、lambda表达式和事件 笔记
本章代码分为以下几个主要的示例文件: 1. 简单委托 2. 冒泡排序 3. lambda表达式 4. 事件示例 5. 弱事件 引用方法 委托是寻址方法的.NET版本.在C++中函数 ...
- 委托、Lambda表达式、事件系列07,使用EventHandler委托
谈到事件注册,EventHandler是最常用的. EventHandler是一个委托,接收2个形参.sender是指事件的发起者,e代表事件参数. □ 使用EventHandler实现猜拳游戏 使用 ...
- 委托、Lambda表达式、事件系列06,使用Action实现观察者模式,体验委托和事件的区别
在"实现观察者模式(Observer Pattern)的2种方式"中,曾经通过接口的方式.委托与事件的方式实现过观察者模式.本篇体验使用Action实现此模式,并从中体验委托与事件 ...
- 委托、Lambda表达式、事件系列04,委托链是怎样形成的, 多播委托, 调用委托链方法,委托链异常处理
委托是多播委托,我们可以通过"+="把多个方法赋给委托变量,这样就形成了一个委托链.本篇的话题包括:委托链是怎样形成的,如何调用委托链方法,以及委托链异常处理. □ 调用返回类型为 ...
- 委托、Lambda表达式、事件系列03,从委托到Lamda表达式
在"委托.Lambda表达式.事件系列02,什么时候该用委托"一文中,使用委托让代码简洁了不少. namespace ConsoleApplication2 { internal ...
- 委托、Lambda表达式、事件系列02,什么时候该用委托
假设要找出整型集合中小于5的数. static void Main(string[] args) { IEnumerable<int> source = new List<int&g ...
- 委托、Lambda表达式、事件系列01,委托是什么,委托的基本用法,委托的Method和Target属性
委托是一个类. namespace ConsoleApplication1 { internal delegate void MyDelegate(int val); class Program { ...
随机推荐
- E. Magic Stones CF 思维题
E. Magic Stones time limit per test 1 second memory limit per test 256 megabytes input standard inpu ...
- C#事件の事件聚合器
事件聚合器用于集中管理事件的订阅(Subscribe)和处理(Handle),要使用事件聚合器,首先要理解:事件(event)本质上是一个类. 传统的+=和-=不足: 1.管理很麻烦:2.不方便扩展. ...
- win10 搭建virtualenvwrapper虚拟环境
1. 安装virtualenvwrapper pip install virtualenvwrapper-win 注: linux下运行pip install virtualenvwrapper 2. ...
- [CQOI2016]手机号码
嘟嘟嘟 这题一看就是数位dp. 我写数位dp,一般是按数位dp的格式写一个爆搜,然后加一点记忆化. 不过其实我一直不是很清楚记忆化是怎么加,感觉就是把dfs里的参数都扔到dp数组里,好像很暴力啊. 这 ...
- 【移动端】icon中ng-cordova使用
cordova介绍 Cordova提供了一组设备相关的API,通过这组API,移动应用能够以JavaScript访问原生的设备功能,如摄像头.麦克风等. Cordova支持如下7种移动操作系统:iOS ...
- Luogu4745/Gym101620G CERC2017 Gambling Guide 期望、DP、最短路
传送门--Luogu 传送门--Vjudge 设\(f_x\)为从\(x\)走到\(N\)的期望步数 如果没有可以不动的限制,就是隔壁HNOI2013 游走 如果有可以不动的限制,那么\(f_x = ...
- Vue-组件使用细节
一.用is指定组件为特定的原生HTML元素. html: <div id="app"> <table> <tbody> <item> ...
- MFC 坦克定位
最近学习MFC,写了个用键盘上下左右移动的坦克界面,效果图: 先用VC++新建一个最简单的MFC项目,基于Dialog的 1. 添加坦克图片资源:略 2. 添加3个变量:x, y, m_bitmap ...
- 一篇 SpringData+JPA 总结
概述 SpringData,Spring 的一个子项目,用于简化数据库访问,支持 NoSQL 和关系数据库存储 SpringData 项目所支持 NoSQL 存储 MongDB(文档数据库) Neo4 ...
- JqGrid分页按钮图标不显示的bug
开发中遇到的一个小问题,记录一下,如果有朋友也遇到了相同的问题,可以少走些弯路少花点时间. 如图: 分页插件使用了JqGrid,但是分页栏里出现了问题,上一页.下一页这些按钮的图标都显示为空,记得以前 ...