C#委托与事件
委托
参考官方文档:
委托具有以下属性:
- 委托类似于 C++ 函数指针,但它们是类型安全的
- 委托允许将方法作为参数进行传递
- 委托可用于定义回调方法
- 委托可以链接在一起;例如,可以对一个事件调用多个方法
委托是类型,就好像类是类型一样。与类一样,委托类型必须在被用来创建变量以及类型的对象之前声明。
委托声明
委托声明决定了可由该委托引用的方法。委托可指向一个与其具有相同标签的方法。
例如,假设有一个委托:
public delegate int MyDelegate (string s);
上面的委托可被用于引用任何一个带有一个单一的
string
参数的方法,并返回一个int
类型变量。
声明委托的语法如下:
delegate <return type> <delegate-name> <parameter list>
委托类型声明:
- 以delegate关键字开头
- 没有方法主体
实例化委托(Delegate)
一旦声明了委托类型,委托对象必须使用 new 关键字来创建,且与一个特定的方法有关。当创建委托时,传递到 new 语句的参数就像方法调用一样书写,但是不带有参数。例如:
public delegate void printString(string s);
...
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);
例子
参考:
这里的例子是,假设有一个Employee
类,有如下的几个属性:
- Id
- Name
- Experience
- Salary
现在在Employee
类中添加一个方法,用来对Employee
升职。该方法使用一个Employee
的List作为参数,输出所有应该升职值的名字。
但是,什么样的Employee
才可以升职呢?我们可以基于Experience
,或基于Salary
,或者其它的条件。所以不能把条件硬编码到方法中
为实现这种形式,我们就使用delegate
。创建一个委托EligibleToPromote
。这个委托把Employee
对象作为参数,返回一个布尔值。
而在PromoteEmployee
方法中,使用Employee
的List和EligibleToPromote
类型的委托作为参数。遍历List中的Employee
,把Employee
传递为委托,如果委托返回true,则该对Employee
进行升职
public delegate bool EligibleToPromote(Employee EmployeeToPromote);
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int Experience { get; set; }
public int Salary { get; set; }
public static void PromoteEmployee(List<Employee> EmployeeList, EligibleToPromote IsEmployeeEligible)
{
foreach (Employee employee in EmployeeList)
{
if (IsEmployeeEligible(employee))
{
Console.WriteLine("Employee {0} Promoted", employee.Name);
}
}
}
}
现在,在使用Employee
类时,就可以灵活的决定如何对员工进行升职。
class Program
{
static void Main(string[] args)
{
Employee E1 = new Employee { Id = 101, Name = "Test 1", Experience = 5, Salary = 5000 };
Employee E2 = new Employee { Id = 102, Name = "Test 2", Experience = 10, Salary = 10000 };
Employee E3 = new Employee { Id = 103, Name = "Test 3", Experience = 20, Salary = 20000 };
List<Employee> employeeList = new List<Employee>();
employeeList.Add(E1);
employeeList.Add(E2);
employeeList.Add(E3);
EligibleToPromote eligible = new EligibleToPromote(Promote);
Employee.PromoteEmployee(employeeList, eligible);
}
private static bool Promote(Employee employee)
{
if (employee.Salary >= 10000)
{
return true;
}
else
{
return false;
}
}
}
注意上面的创建的Promote
方法,该方法中就是升职的逻辑。这个方法之后被传给了委托,作为一个参数。注意该方法的signature与EligibleToPromote
委托是一致的。
调试,输出结果为:
Employee Test 2 Promoted
Employee Test 3 Promoted
合并委托
委托对象的一个有用属性在于可通过使用 +
运算符将多个对象分配到一个委托实例。多播委托包含已分配委托列表。 此多播委托被调用时会依次调用列表中的委托。 仅可合并类型相同的委托。
-
运算符可用于从多播委托中删除组件委托
官方的例子:
// 定义一个自定义委托,有一个string类型参数,返回值为void
delegate void CustomDel(string s);
class Program
{
// 定义2个与CustomDel签名一样的方法
static void Hello(string s)
{
System.Console.WriteLine(" Hello, {0}!", s);
}
static void Goodbye(string s)
{
System.Console.WriteLine(" Goodbye, {0}!", s);
}
static void Main()
{
// 声明自定义委托的实例.
CustomDel hiDel, byeDel, multiDel, multiMinusHiDel;
hiDel = Hello;
byeDel = Goodbye;
// 合并委托
multiDel = hiDel + byeDel;
// 移除委托
multiMinusHiDel = multiDel - hiDel;
Console.WriteLine("Invoking delegate hiDel:");
hiDel("A");
Console.WriteLine("Invoking delegate byeDel:");
byeDel("B");
Console.WriteLine("Invoking delegate multiDel:");
multiDel("C");
Console.WriteLine("Invoking delegate multiMinusHiDel:");
multiMinusHiDel("D");
}
}
输出结果为:
Invoking delegate hiDel:
Hello, A!
Invoking delegate byeDel:
Goodbye, B!
Invoking delegate multiDel:
Hello, C!
Goodbye, C!
Invoking delegate multiMinusHiDel:
Goodbye, D!
运算符+=
为委托添加方法
运算符-=
从委托移除方法
调用带返回值的委托
如果委托有返回值并且在调用列表中有一个以上的方法,会发生下面的情况:
- 调用列表中最后一个方法返回的值就是委托调用返回的值
- 调用列表中所有其他方法的返回值都会被忽略
如下:
public delegate int MyDel();//声明有返回值的方法
public class MyClass
{
int IntValue = 5;
public int Add2() { IntValue += 2; return IntValue; }
public int Add3() { IntValue += 3; return IntValue; }
static void Main()
{
MyClass mc = new MyClass();
MyDel mDel = mc.Add2;//创建并初始化委托
mDel += mc.Add3;//增加方法
mDel += mc.Add2;//增加方法
Console.WriteLine("Value: {0}", mDel());
}
}
其输出结果为:
Value: 12
调用带引用参数的委托
如果委托有引用委托,参数值会根据调用列表中的一个或多个方法的返回值而改变
- 在调用委托列表中的下一个方法时,参数的新值(不是初始值)会传给下一个方法
如下:
delegate void MyDel02(ref int x);
public class MyClass02
{
public void Add2(ref int x) { x += 2; }
public void Add3(ref int x) { x += 3; }
static void Main()
{
MyClass02 mc = new MyClass02();
MyDel02 mDel = mc.Add2;
mDel += mc.Add3;
mDel += mc.Add2;
int x = 5;
mDel(ref x);
Console.WriteLine("Value: {0}",x);
}
}
输出结果如下:
Value: 12
匿名方法
在程序中有这样的需求,需要一个临时方法,这个方法只会使用一次,或者使用的很少,这个方法的方法体很短,没有必要创建独立的具名方法。匿名方法允许我们避免使用独立的具名方法。
匿名方法(anonymous method
)是在初始化委托时内联(inline)声明的方法
如下的2个方法是等价的
1.声明了一个Add20的方法
class Delegate
{
public static int Add20(int x)
{
return x + 20;
}
delegate int OtherDel(int InParam);
static void Main()
{
OtherDel del = Add20;
Console.WriteLine("{0}", del(5));
}
}
2.使用匿名方法来代替
class Delegate
{
delegate int OtherDel(int InParam);
static void Main()
{
OtherDel del = delegate(int x)
{
return x += 20;
};
Console.WriteLine("{0}", del(5));
}
}
匿名方法的语法
匿名方法表达式的语法包含如下的组成部分:
- delegate类型关键字
- 参数列表,如果语句块没有使用任何参数则可以省略
- 语句块,它包含了匿名方法的代码
delegate (参数列表) {语句块}
返回类型:匿名方法不会显示声明返回值。然而,实现代码本身的行为必须通过返回一个在类型上与委托的返回类型相同的值来匹配委托的返回类型。如果委托有void类型的返回值,匿名方法就不能有返回值
使用匿名方法
在如下的地方使用匿名方法:
1.声明委托变量时作为初始化表达式
2.组合委托时在复制语句的右边
3.为委托增加事件时在赋值语句的右边
Lambda表达式
C#2.0引入匿名方法,然而它的语法有一点麻烦,而且需要一些编译器已经知道的信息。C#3.0引入了Lambda表达式,简化了匿名方法的语法,从而避免了多余的信息。可以使用Lambda表达式来替换匿名方法。
在匿名方法的语法中,delegate
关键字是有点多余的,因为编译器已经知道我们将在方法赋值给委托。可以很容易通过如下的步骤把匿名方法转换为Lambda
表达式
- 删除delegate关键字
- 在参数列表和匿名方法主体之间放Lambda运算符
=>
MyDel del = delegate(int x) {return x+ 1;};//匿名方法
MyDel lel = (int x) => { return x + 1; };//Lambda表达式
Lambda表达式的要点如下:
- Lambda表达式参数列表中的参数必须在参数数量、类型和位置上与委托匹配
- 表达式的参数列表中的参数不一定需要包含类型(隐式类型)。除非委托有ref或out参数——此时必须注明类型(显式类型)
- 如果只有一个参数,并且是隐式类型的,周围的园括号可以被省略,否则必须有括号
- 如果没有参数,必须使用一组空的园括号
如下的例子:
delegate double MyDelegate(int par);
class Program
{
static void Main(string[] args)
{
MyDelegate del = delegate (int x) { return x + 1; };//匿名方法
MyDelegate le1 = (int x) => { return x + 1; };
MyDelegate le2 = (x) => { return x + 1; };
MyDelegate le3 = x => { return x + 1; };
MyDelegate le4 = x => x + 1;
Console.WriteLine("{0}", le1(12));
Console.WriteLine("{0}", le2(12));
Console.WriteLine("{0}", le3(12));
Console.WriteLine("{0}", le4(12));
}
}
Action、Func委托
其它介绍:C#委托的介绍(delegate、Action、Func、predicate)
Action
是无返回值的泛型委托,参考文档Action 委托
Func是有返回值的泛型委托,参考文档Func
事件
很多程序都有一个共同的需求,即当一个特定的程序事件发生时,程序的其它部分可以得到该事件已经发生的通知。
发布者/订阅者模式可以满足这种需求:
- 发布者(publisher)发布某个事件的类或结构,其它类可以在该事件发生时得到通知
- 订阅者(subscriber)注册并在事件发生时得到通知的类或结构
- 事件处理程序(event handler)由订阅者注册到事件的方法,在发布者触发事件时执行
- 触发(raise)事件 当事件触发时,所有注册到它的方法都会被依次调用
声明事件
使用event
关键字
//声明委托
delegate void Handler();
class Incrementer
{
//创建事件并发布
public event Handler CountedADozen;
}
订阅事件
订阅者向事件添加处理程序。对于一个要添加到事件的事件处理程序来说。它必须具有与事件的委托相同的返回类型和签名。
使用+=运算符来为事件增加事件处理程序。
触发事件
事件成员本身只是保存了需要被调用的事件处理程序。如果事件没有被触发,什么都不会发生。
一些注意事项:
- 在触发事件之前,检查事件处理程序是否为null
如下的例子:
class Event
{
//声明委托
delegate void Handler();
class Incrementer
{
//创建事件并发布
public event Handler CountedADozen;
public void DoCount()
{
for (int i = 1; i < 100; i++)
{
if (i % 12 == 0 && CountedADozen != null)
{
//每增加12个计数器触发事件一次
CountedADozen();
}
}
}
}
class Dozens
{
public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer)
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount;
}
void IncrementDozensCount()
{
DozensCount++;
Console.WriteLine("DozensCount: {0}", DozensCount);
}
}
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozensCounter = new Dozens(incrementer);//订阅事件
incrementer.DoCount();
}
}