C#基础之委托,事件
1 委托
1.1 简介
C# 中的委托(Delegate
)类似于 C 或 C++ 中函数的指针。委托(Delegate
) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。
委托(Delegate
)特别用于实现事件
和回调
方法。所有的委托(Delegate
)都派生自 System.Delegate
类。
1.2 操作使用
1.2.1 声明委托(Delegate)
委托声明决定了可由该委托引用的方法。委托可指向一个与其具有相同标签的方法。
例如,假设有一个委托:public delegate int MyDelegate (string s);
上面的委托可被用于引用任何一个带有一个单一的 string 参数的方法,并返回一个 int 类型变量
声明委托的语法如下:
delegate <return type> <delegate-name> <parameter list>
1.2.2 实例化委托(Delegate)
一旦声明了委托类型,委托对象必须使用 new
关键字来创建,且与一个特定的方法有关。当创建委托时,传递到 new
语句的参数就像方法调用一样书写,但是不带有参数
。例如:
public delegate void printString(string s);
...
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);
下面的实例演示了委托的声明、实例化和使用,该委托可用于引用带有一个整型参数的方法,并返回一个整型值。
using System;
delegate int NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static int AddNum(int p)
{
num += p;
return num;
}
public static int MultNum(int q)
{
num *= q;
return num;
}
public static int getNum()
{
return num;
}
static void Main(string[] args)
{
// 创建委托实例
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
// 使用委托对象调用方法
nc1(25);
Console.WriteLine("Value of Num: {0}", getNum());
nc2(5);
Console.WriteLine("Value of Num: {0}", getNum());
Console.ReadKey();
}
}
}
结果:
Value of Num: 35
Value of Num: 175
注意
:调用委托对应方法一般是通过invoke
方法,但是从 C# 2.0
开始,委托的调用可以直接使用方法调用语法,而不需要显式调用 Invoke
方法。
1.2.3 直接调用和invoke
虽然委托调用底层实际上是通过 Invoke
方法实现的,但语法上允许直接调用委托,就像调用普通方法一样。换句话说,调用委托
和直接调用 Invoke
方法是等效的。
假设我们有一个 Action 类型的委托:
Action action = () => Console.WriteLine("Hello, Delegate!");
直接调用委托:
action(); // 输出: Hello, Delegate!
通过 Invoke 方法调用:
action.Invoke(); // 输出: Hello, Delegate!
两种方式的结果完全一样,因为 ()
是对委托对象 Invoke
方法的简化语法糖
。
为什么允许直接调用?
简洁性
:如果每次调用都必须写 .Invoke,代码显得冗长。因此,C# 提供了直接调用语法,增强代码可读性。语法糖
:编译器在编译时会自动将直接调用委托语法转换为 Invoke 方法的调用。
即:action();
实际被编译为:action.Invoke();
- 优先推荐直接调用
直接调用的方式更加简洁可读,因此在大多数情况下,推荐使用action()
而不是显式调用action.Invoke()
。
为什么保留 Invoke 方法?虽然直接调用语法更方便,但在某些特殊场景下,显式调用 Invoke 方法可能更合适:
反射场景
:通过反射调用委托时,需要使用Invoke
方法。动态场景
:在动态生成代码或动态委托时,Invoke
方法更明确。
using System;
using System.Reflection;
class Program
{
static void Main()
{
Action action = PrintMessage;
// 使用反射调用 Invoke
MethodInfo invokeMethod = action.GetType().GetMethod("Invoke");
invokeMethod.Invoke(action, null);
}
static void PrintMessage()
{
Console.WriteLine("Hello, Reflection!");
}
}
输出:
Hello, Reflection!
1.2.4 Invoke 和 BeginInvoke
委托的 Invoke
和 BeginInvoke
方法分别用于同步
和异步
调用委托。它们的主要区别体现在调用方式、线程管理和返回结果的处理上。
Invoke 和 BeginInvoke 的区别
特性 | Invoke | BeginInvoke |
---|---|---|
调用类型 | 同步调用 | 异步调用 |
线程阻塞 | 当前线程会阻塞,直到方法执行完成 | 当前线程不会阻塞 |
返回结果 | 直接返回方法的返回值 | 返回 IAsyncResult 对象,通过 EndInvoke 获取返回值 |
异常处理 | 异常会直接在调用线程中抛出 | 异常在调用 EndInvoke 时抛出 |
线程使用 | 在调用线程上执行方法 | 在线程池中执行方法 |
使用场景 | 方法较快且调用线程不能被中断时 | 方法较慢且需要异步执行时 |
- Invoke:同步调用
定义:Invoke
是同步调用,当前线程会等待方法执行完毕后再继续执行后续代码。
特点:- 阻塞调用:调用线程会被阻塞,直到被调用的方法完成。
- 返回结果:直接返回被调用方法的返回值(如果有)。
- 异常处理:如果被调用的方法抛出异常,异常会在调用线程中传播。
// 定义一个委托
delegate int AddDelegate(int x, int y);
AddDelegate add = (x, y) => x + y;
// 同步调用
int result = add.Invoke(3, 4);
Console.WriteLine($"Result: {result}"); // 输出:Result: 7
- BeginInvoke:异步调用
定义:BeginInvoke
是异步调用,立即返回一个 IAsyncResult 对象,并不会阻塞调用线程。
特点:- 非阻塞调用:调用线程可以继续执行其他代码,而被调用的方法在后台线程中执行。
- 回调机制:可以通过传递回调方法或轮询 IAsyncResult 对象来获取结果。
需要显式调用 EndInvoke 方法以获取结果或处理异常。
// 定义一个委托
delegate int AddDelegate(int x, int y);
AddDelegate add = (x, y) =>
{
Console.WriteLine("Adding...");
System.Threading.Thread.Sleep(2000); // 模拟耗时操作
return x + y;
};
// 异步调用
IAsyncResult asyncResult = add.BeginInvoke(3, 4, null, null);
// 主线程继续执行其他任务
Console.WriteLine("Doing other work...");
// 获取异步调用结果
int result = add.EndInvoke(asyncResult);
Console.WriteLine($"Result: {result}"); // 输出:Result: 7
BeginInvoke 的回调,可以通过回调函数在异步操作完成后处理结果:
void CallbackMethod(IAsyncResult ar)
{
// 获取委托实例
AddDelegate add = (AddDelegate)ar.AsyncState;
// 获取结果
int result = add.EndInvoke(ar);
Console.WriteLine($"Result in Callback: {result}");
}
AddDelegate add = (x, y) =>
{
Console.WriteLine("Adding...");
System.Threading.Thread.Sleep(2000);
return x + y;
};
// 异步调用并指定回调函数
add.BeginInvoke(5, 7, CallbackMethod, add);
// 主线程继续工作
Console.WriteLine("Doing other work...");
注意事项:
- BeginInvoke 使用线程池中的线程来执行方法,因此需要注意线程池的资源消耗。
必须调用 EndInvoke: - 调用 BeginInvoke 后,无论是否需要结果,都必须调用 EndInvoke,否则可能会导致资源泄漏。
- 推荐使用 Task 和 async/await:
- 在现代 C# 中,推荐使用 Task 和
async/await
替代BeginInvoke
和EndInvoke
,因为它们更易读且不易出错。
1.3 委托的多播
委托对象可使用 +
运算符进行合并。一个合并委托调用它所合并的两个委托。只有相同类型的委托可被合并。-
运算符可用于从合并的委托中移除组件委托。
使用委托的这个有用的特点,可以创建一个委托被调用时要调用的方法的调用列表。这被称为委托的 多播(multicasting)
,也叫组播
。
+
和 -
运算符确实可以直接用于委托对象的合并和移除,但这和 +=
和 -=
的用法有所不同。它们的区别主要在于运算场景
和赋值方式
。具体来说
+
和-
运算符:用于直接创建新的委托对象,不影响原始委托。它们不会修改原始委托,而是生成一个新的多播委托对象。- 使用
+
合并两个委托对象,生成一个新的多播委托。 - 使用
-
从多播委托中移除一个委托,生成一个新的委托对象。
- 使用
+=
和-=
运算符:用于修改已有的委托实例,直接在原始变量上添加或移除委托。+=
将一个委托添加到现有委托链上,结果赋给原变量。-=
从现有委托链中移除一个委托,结果赋给原变量。
下面的程序演示了委托的多播:
using System;
delegate int NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static int AddNum(int p)
{
num += p;
return num;
}
public static int MultNum(int q)
{
num *= q;
return num;
}
public static int getNum()
{
return num;
}
static void Main(string[] args)
{
// 创建委托实例
NumberChanger nc;
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
nc = nc1;
nc += nc2;
// 调用多播
nc(5);
Console.WriteLine("Value of Num: {0}", getNum());
Console.ReadKey();
}
}
}
结果:
Value of Num: 75
注意
:
在C#中,当使用+=
操作符向委托添加方法时,有两种方式是等效的:
- 显式地创建一个新的委托实例并将其添加到现有的委托链中
myDelegate += new NumberChanger(AddNum); - 省略new 部分,直接添加方法。
C#
编译器会自动为您处理委托的实例化(如果必要的话):myDelegate += AddNum
这两种方式在功能上是完全相同的。从C# 2.0开始,第二种方式(省略new关键字和委托类型)变得更加流行,因为它更简洁,并且减少了不必要的代码。
1.4 委托的匿名和lambda
二者比较:
特性 | 匿名方法 (delegate) | Lambda 表达式 |
---|---|---|
语法简洁性 | 较繁琐,需要显式写出 delegate 关键字和参数列表 |
更简洁,直接用 (参数) => {} 表达逻辑 |
表达式形式支持 | 不支持表达式形式,必须用 {} 包裹逻辑块 | 支持表达式形式,单行逻辑可以省略 {} 和 return |
捕获外部变量(闭包) | 支持 | 支持 |
语法风格 | 更接近传统 C# 方法声明 | 更现代、函数式编程风格 |
语义清晰性 | delegate 明确表明它是匿名方法 |
使用 => 运算符,强调简洁和函数式思想 |
1.4.1 匿名方法
匿名方法是通过使用 delegate
关键字创建委托实例来声明的。
语法
delegate(parameters) { statement; }
例如:
delegate void NumberChanger(int n);
...
NumberChanger nc = delegate(int x)
{
Console.WriteLine("Anonymous Method: {0}", x);
};
代码块 Console.WriteLine("Anonymous Method: {0}", x);
是匿名方法的主体。
委托可以通过匿名方法调用,也可以通过命名方法调用,即,通过向委托对象传递方法参数。
using System;
delegate void NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static void AddNum(int p)
{
num += p;
Console.WriteLine("Named Method: {0}", num);
}
public static void MultNum(int q)
{
num *= q;
Console.WriteLine("Named Method: {0}", num);
}
static void Main(string[] args)
{
// 使用匿名方法创建委托实例
NumberChanger nc = delegate(int x)
{
Console.WriteLine("Anonymous Method: {0}", x);
};
// 使用匿名方法调用委托
nc(10);
// 使用命名方法实例化委托
nc = new NumberChanger(AddNum);
// 使用命名方法调用委托
nc(5);
// 使用另一个命名方法实例化委托
nc = new NumberChanger(MultNum);
// 使用命名方法调用委托
nc(2);
Console.ReadKey();
}
}
}
1.4.2 lambda 表达式
在 C# 2.0 及更高版本中,引入了 lambda 表达式,它是一种更简洁的语法形式,用于编写匿名方法。并且 从 C# 2.0
开始对委托的实例化做了简化,委托类型的实例化在某些情况下可以省略显式使用 new
关键字
使用 lambda 表达式:
using System;
delegate void NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static void AddNum(int p)
{
num += p;
Console.WriteLine("Named Method: {0}", num);
}
public static void MultNum(int q)
{
num *= q;
Console.WriteLine("Named Method: {0}", num);
}
static void Main(string[] args)
{
// 使用 lambda 表达式创建委托实例
NumberChanger nc = x => Console.WriteLine($"Lambda Expression: {x}");
// 使用 lambda 表达式调用委托
nc(10);
// 使用命名方法实例化委托
nc = new NumberChanger(AddNum);
// 使用命名方法调用委托
nc(5);
// 使用另一个命名方法实例化委托
nc = new NumberChanger(MultNum);
// 使用命名方法调用委托
nc(2);
Console.ReadKey();
}
}
}
1.5 内置委托
C# 提供了一些内置的泛型委托,可以覆盖大部分常见场景,主要包括以下几个
1.5.1 Action系列
Action
是一个用于定义没有返回值的方法的委托。支持最多 16 个参数的重载。
Action action = () => Console.WriteLine("No parameters");
action();
Action<int, string> actionWithParams = (x, y) => Console.WriteLine($"x: {x}, y: {y}");
actionWithParams(10, "hello");
1.5.2 Func 系列
Func
是一个带有返回值的泛型委托。最多支持 16 个输入参数,最后一个泛型参数是返回值的类型,前面的泛型参数表示输入参数
Func<int, int, int> add = (x, y) => x + y;
int result = add(3, 5);
Console.WriteLine(result); // 输出 8
1.5.3 Predicate
Predicate<T>
是一个返回 bool
的泛型委托,常用于过滤或条件判断。
Predicate<int> isEven = x => x % 2 == 0;
bool check = isEven(4);
Console.WriteLine(check); // 输出 True
1.6 示例
下面的实例演示了委托的用法。委托 printString 可用于引用带有一个字符串作为输入的方法,并不返回任何东西。
我们使用这个委托来调用两个方法,第一个把字符串打印到控制台,第二个把字符串打印到文件:
using System;
using System.IO;
namespace DelegateAppl
{
class PrintString
{
static FileStream fs;
static StreamWriter sw;
// 委托声明
public delegate void printString(string s);
// 该方法打印到控制台
public static void WriteToScreen(string str)
{
Console.WriteLine("The String is: {0}", str);
}
// 该方法打印到文件
public static void WriteToFile(string s)
{
fs = new FileStream("c:\\message.txt", FileMode.Append, FileAccess.Write);
sw = new StreamWriter(fs);
sw.WriteLine(s);
sw.Flush();
sw.Close();
fs.Close();
}
// 该方法把委托作为参数,并使用它调用方法
public static void sendString(printString ps)
{
ps("Hello World");
}
static void Main(string[] args)
{
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);
sendString(ps1);
sendString(ps2);
Console.ReadKey();
}
}
}
结果:
The String is: Hello World
2 事件
2.1 简介
C# 事件(Event
)是一种成员,用于将特定的事件通知发送给订阅者。事件通常用于实现观察者模式
,它允许一个对象将状态的变化通知其他对象,而不需要知道这些对象的细节。
事件(Event
) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件。例如,中断。
C# 中使用事件机制实现线程间的通信。
关键点:
- 声明委托:定义事件将使用的委托类型。委托是一个函数签名。
- 声明事件:使用
event
关键字声明一个事件。 - 触发事件:在适当的时候调用事件,通知所有订阅者。
- 订阅和取消订阅事件:其他类可以通过
+=
和-=
运算符订阅和取消订阅事件。
事件模型五个组成部分:
- 事件的拥有者
- 事件成员
- 事件的响应者
- 事件处理器
- 事件订阅--把事件处理器与事件关联在一起,本质是一种以委托类型为基础的
2.2 原理
2.2.1 讲解
事件在类中声明且生成,且通过使用同一个类或其他类中的委托
与事件
处理程序关联。包含事件的类用于发布事件。这被称为 发布器(publisher
) 类。其他接受该事件的类被称为 订阅器(subscriber
) 类。事件使用 发布-订阅(publisher-subscriber
) 模型。
- 发布器(
publisher
) 是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器类的对象调用这个事件,并通知其他的对象。 - 订阅器(
subscriber
) 是一个接受事件并提供事件处理程序的对象。在发布器类中的委托调用订阅器(subscriber
)类中的方法(事件处理程序)
在C#中,通常使用+=
操作符来订阅事件,使用-=
操作符来取消订阅事件
2.2.2 add 和 remove 访问器
自己定义事件的 add
和 remove
访问器,从而控制事件订阅和取消订阅的具体行为。
下面的 EventHandler 是系统自带 事件,不用声明
public class EventDemo
{
private EventHandler _myEvent;
// 自定义事件
public event EventHandler MyEvent
{
add
{
Console.WriteLine("Adding a subscriber");
_myEvent += value;
}
remove
{
Console.WriteLine("Removing a subscriber");
_myEvent -= value;
}
}
public void TriggerEvent()
{
_myEvent?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
EventDemo demo = new EventDemo();
EventHandler handler = (sender, e) => Console.WriteLine("Event triggered!");
// 订阅事件
demo.MyEvent += handler; // 输出: Adding a subscriber
// 触发事件
demo.TriggerEvent(); // 输出: Event triggered!
// 取消订阅事件
demo.MyEvent -= handler; // 输出: Removing a subscriber
}
}
自定义 add 和 remove 访问器通常在以下场景中使用:
- 自定义订阅逻辑:需要记录订阅者或对订阅者进行筛选时。
- 线程安全:确保事件的订阅和取消订阅在多线程环境下安全。
- 限制订阅数量:控制最多只能有特定数量的订阅者。
- 日志记录或调试:每次事件订阅或取消时记录相关信息。
注意事项:
- 事件是委托的包装:事件是基于委托的,但它对委托的直接访问进行了限制,提供了一种更安全的机制来管理委托调用。
- 事件默认行为:如果不需要特殊逻辑,直接使用默认的 add 和 remove,即可满足大部分场景。
- 不要直接对事件赋值:只能通过
+=
和-=
访问事件。直接赋值(如 MyEvent = null)是不允许的,除非是在声明类内部。
2.3 使用原生委托
namespace EventExample
{
class Program
{
MyForm form = new MyForm();
// 写此处原生对应的 事件可以先写此处名字,让visualstudio 自动生成对应参数类型的 事件
form.Click += form.FormClicked;
form.ShowDialog();
}
class MyForm : Form
{
internal void FormClicked(object sender,EventArgs e)
{
this.Text = DataTime.Now.ToString();
}
}
}
2.4 自定义委托
2.4.1 声明
在类的内部声明事件,首先必须声明该事件的委托类型。
例如:
public delegate void BoilerLogHandler(string status);
然后,声明事件本身,使用 event
关键字:
// 基于上面的委托定义事件
public event BoilerLogHandler BoilerEventLog;
上面的代码定义了一个名为 BoilerLogHandler 的委托和一个名为 BoilerEventLog 的事件,该事件在生成的时候会调用委托。
2.4.2 操作
2.4.2.1 示例一
以下示例展示了如何在 C# 中使用事件:
using System;
namespace EventDemo
{
// 定义一个委托类型,用于事件处理程序
public delegate void NotifyEventHandler(object sender, EventArgs e);
// 发布者类
public class ProcessBusinessLogic
{
// 声明事件
public event NotifyEventHandler ProcessCompleted;
// 触发事件的方法
protected virtual void OnProcessCompleted(EventArgs e)
{
ProcessCompleted?.Invoke(this, e);
}
// 模拟业务逻辑过程并触发事件
public void StartProcess()
{
Console.WriteLine("Process Started!");
// 这里可以加入实际的业务逻辑
// 业务逻辑完成,触发事件
OnProcessCompleted(EventArgs.Empty);
}
}
// 订阅者类
public class EventSubscriber
{
public void Subscribe(ProcessBusinessLogic process)
{
process.ProcessCompleted += Process_ProcessCompleted;
}
private void Process_ProcessCompleted(object sender, EventArgs e)
{
Console.WriteLine("Process Completed!");
}
}
class Program
{
static void Main(string[] args)
{
ProcessBusinessLogic process = new ProcessBusinessLogic();
EventSubscriber subscriber = new EventSubscriber();
// 订阅事件
subscriber.Subscribe(process);
// 启动过程
process.StartProcess();
Console.ReadLine();
}
}
}
说明
- 定义委托类型:
public delegate void NotifyEventHandler(object sender, EventArgs e);
这是一个委托类型,它定义了事件处理程序的签名。通常使用EventHandler
或EventHandler<TEventArgs>
来替代自定义的委托。 - 声明事件:
public event NotifyEventHandler ProcessCompleted;
这是一个使用 NotifyEventHandler 委托类型的事件。 - 触发事件:
protected virtual void OnProcessCompleted(EventArgs e)
{
ProcessCompleted?.Invoke(this, e);
}
这是一个受保护的方法,用于触发事件。使用 ?.Invoke
语法来确保只有在有订阅者时才调用事件。
- 订阅和取消订阅事件:
process.ProcessCompleted += Process_ProcessCompleted;
订阅者使用+=
运算符订阅事件,并定义事件处理程序 Process_ProcessCompleted。
2.4.2.2 示例二
using System;
namespace SimpleEvent
{
using System;
/***********发布器类***********/
public class EventTest
{
private int value;
public delegate void NumManipulationHandler();
public event NumManipulationHandler ChangeNum;
protected virtual void OnNumChanged()
{
if ( ChangeNum != null )
{
ChangeNum(); /* 事件被触发 */
}else {
Console.WriteLine( "event not fire" );
Console.ReadKey(); /* 回车继续 */
}
}
public EventTest()
{
int n = 5;
SetValue( n );
}
public void SetValue( int n )
{
if ( value != n )
{
value = n;
OnNumChanged();
}
}
}
/***********订阅器类***********/
public class subscribEvent
{
public void printf()
{
Console.WriteLine( "event fire" );
Console.ReadKey(); /* 回车继续 */
}
}
/***********触发***********/
public class MainClass
{
public static void Main()
{
EventTest e = new EventTest(); /* 实例化对象,第一次没有触发事件 */
subscribEvent v = new subscribEvent(); /* 实例化对象 */
e.ChangeNum += new EventTest.NumManipulationHandler( v.printf ); /* 注册 */
e.SetValue( 7 );
e.SetValue( 11 );
}
}
}
结果:
event not fire
event fire
event fire