委托是C#编程语言中的一个重要特性,它是一种类似于函数指针的引用类型变量,它可以存储对一个或多个方法的引用。
委托可以在运行时被动态地改变,使得我们可以将方法作为参数进行传递,以及在需要时动态地调用它们,从而实现回调、事件处理和异步编程等功能。
在 C# 中,委托是一种引用类型变量,它的声明使用 delegate
关键字,其语法格式为:
<访问修饰符> delegate <返回类型> <委托名称>([参数列表]);
其中:
internal
;例如,下面是声明一个简单的委托类型的示例:
delegate void MyDelegate(string name);
该委托类型可以引用一个参数为字符串类型、无返回值的方法。上述委托的访问等级为默认的 internal
,这意味着仅同一个程序集内的其他代码可以访问该委托类型,但是在程序集之外的代码则不能直接访问该委托类型。
委托的使用可以分为以下几步:
一旦声明了委托类型,委托对象需要使用 new
关键字来创建,且与一个特定的方法有关。当创建委托时,传递到 new
语句的参数就像方法调用一样书写,但是不需要在创建时传递参数。例如:
public delegate void printString(string s);
...
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);
以下是委托的声明、实例化并调用的示例代码:
using System;
// 声明一个委托类型
delegate int Calculate(int a, int b);
class Program
{
static int Add(int a, int b)
{
return a + b;
}
static int Multiply(int a, int b)
{
return a * b;
}
static void Main(string[] args)
{
// 创建委托实例,并将其指向Add方法
Calculate calc = Add;
// 使用委托调用Add方法
int result = calc(3, 5);
Console.WriteLine(result); // 输出 8
// 将委托实例重新指向Multiply方法
calc = Multiply;
// 使用委托调用Multiply方法
result = calc(3, 5);
Console.WriteLine(result); // 输出 15
}
}
在这个示例中,我们首先声明了一个委托类型 Calculate
,其定义了一个接收两个 int 参数并返回一个 int 类型值的方法签名。
然后,我们定义了两个静态方法 Add
和 Multiply
,用于执行加法和乘法运算。接下来,我们创建了一个委托实例 calc
并将其指向 Add
方法,然后使用 calc
调用了 Add
方法并输出结果。接着,我们将 calc
重新指向 Multiply
方法,并再次使用 calc
调用了 Multiply
方法并输出结果。
using System;
delegate void MyDelegate(string message);
MyDelegate d1 = Method1; //这种语法,是委托实例化的一种快捷方式,与下方等价
MyDelegate d1 = new MyDelegate(Method1);
需要注意的是,这种快捷方式只适用于将委托实例化为一个方法的情况。对于需要使用 new
语法实例化的其他类型,不能使用这种快捷方式。例如,如果要实例化一个自定义类的对象,仍然需要使用完整的 new
语法。
System.Delegate
是所有委托类型的基类,它提供了一些有用的方法,使得对委托的使用更加方便。以下是System.Delegate
类的一些主要功能:
Combine
:用于将两个委托合并成一个新的委托,使得调用新的委托时,会依次调用原始的两个委托。例如,可以使用Combine方法将两个事件处理方法组合成一个委托,用于处理事件。
Remove
:用于从一个委托中移除另一个委托。如果将一个委托从另一个委托中移除后,调用新的委托时就不会调用移除的委托了。
Equals
:用于比较两个委托是否相等。如果两个委托调用的是同一个方法,它们就是相等的。
GetInvocationList
:用于获取一个委托中引用的所有方法的列表。如果一个委托包含多个方法,可以使用GetInvocationList
方法获取它们的列表。
下面是关于委托的示例代码,演示了 Combine、Remove、Equals、GetInvocationList 方法的用法:
using System;
delegate void DelegateType();
class Program
{
static void Main(string[] args)
{
DelegateType handler1 = new DelegateType(Method1);
DelegateType handler2 = new DelegateType(Method2);
DelegateType handler3 = new DelegateType(Method3);
// Combine two delegates
DelegateType combined = handler1 + handler2;
Console.WriteLine("Combined delegate calls:");
combined();
// Remove a delegate
combined -= handler2;
Console.WriteLine("Combined delegate after removal:");
combined();
// Check if two delegates are equal
bool equals = handler1.Equals(combined);
Console.WriteLine($"handler1 equals combined? {equals}");
// Get an array of invocation list delegates
Delegate[] invocationList = combined.GetInvocationList();
Console.WriteLine("Invocation list:");
foreach (Delegate del in invocationList)
{
del.DynamicInvoke();
}
}
static void Method1()
{
Console.WriteLine("Method 1 called.");
}
static void Method2()
{
Console.WriteLine("Method 2 called.");
}
static void Method3()
{
Console.WriteLine("Method 3 called.");
}
}
输出结果为:
Combined delegate calls:
Method 1 called.
Method 2 called.
Combined delegate after removal:
Method 1 called.
handler1 equals combined? True
Invocation list:
System.DelegateType Method1()
除此之外,System.Delegate
还提供了一些与安全性相关的方法,如 DynamicInvoke
和 DynamicInvokeImpl
,这些方法允许在运行时动态地调用委托,并检查调用的方法是否具有正确的参数和返回类型。
主要介绍一下委托的多播、匿名委托和 Lambda 表达式。
在C#中,委托还具有多播(Multicast)能力,即一个委托对象可以包含多个方法的引用,可以将多个委托对象合并为一个委托对象,并一次性调用所有包含的方法。这使得委托在事件处理中尤其有用,因为多个方法可以同时响应同一个事件。
"+="
运算符将一个委托与另一个委托连接起来,形成一个包含多个方法引用的新委托。类似地,使用 "-="
运算符可以将委托从多播委托中移除。Combine
和 Remove
两个方法。以下是一个简单的示例代码,演示了如何创建、合并和移除委托的多播链:
using System;
delegate void MyDelegate(string message);
class Program
{
static void Main(string[] args)
{
MyDelegate d1 = Method1;
MyDelegate d2 = Method2;
MyDelegate d3 = Method3;
MyDelegate d = d1 + d2 + d3; // 创建多播委托
d("Hello, world!"); // 调用多播委托
d = d - d2; // 移除 d2 委托
d("Hello again!"); // 调用修改后的多播委托
}
static void Method1(string message)
{
Console.WriteLine("Method1 says: " + message);
}
static void Method2(string message)
{
Console.WriteLine("Method2 says: " + message);
}
static void Method3(string message)
{
Console.WriteLine("Method3 says: " + message);
}
}
这段代码的运行结果为:
Method1 says: Hello, world!
Method2 says: Hello, world!
Method3 says: Hello, world!
Method1 says: Hello again!
Method3 says: Hello again
匿名委托和Lambda表达式是两种创建委托实例的方式。
delegate int Calculate(int x, int y);
// 创建匿名委托实例
Calculate calc = delegate (int x, int y) {
return x + y;
};
参数列表 => 方法体
,例如:// 创建Lambda表达式委托实例
Calculate calc = (x, y) => x + y;
Lambda 表达式的优势在于可以大大简化代码,让代码更加易读和易懂。此外,Lambda 表达式也支持 LINQ 等一些强大的功能。
在 C# 中,事件是一种特殊的委托,它允许一个对象在发生某个特定的动作或状态改变时,通知其他对象对这个事件进行响应和处理。例如,当用户点击一个按钮时,系统会触发一个 Click 事件,应用程序可以通过响应该事件来实现对用户的操作。
事件机制的作用在于实现对象间的解耦,也就是将事件的发布者和订阅者解耦。事件让发布者不需要知道谁或哪些对象对事件感兴趣,也不需要直接调用特定的对象或函数来响应事件。事件只需要向外部通知事件发生即可,而订阅者需要注册事件处理器来响应事件。这样一来,发布者与订阅者之间就可以保持独立,且可以随意变换,使得代码更加灵活、可维护性更高。
在 C# 中,声明和使用事件通常需要以下步骤:
事件委托类型定义了事件处理q器的方法签名(包括方法的名称、返回类型以及参数类型和顺序等信息)。在 C# 中,可以使用 delegate
关键字定义事件委托类型。例如,以下代码定义了一个事件委托类型 EventHandler
:
public delegate void EventHandler(object sender, EventArgs e);
这个委托类型有两个参数:
object
类型的 sender
,用于指示事件的来源;EventArgs
类型的 e
,用于传递事件的附加信息。在使用事件时,可以使用这个委托类型作为事件的类型。
在一个类中声明一个事件,需要使用 event
关键字和事件委托类型。例如,以下代码声明了一个名为 ButtonClick
的事件:
public event EventHandler ButtonClick;
这个事件使用了之前定义的 EventHandler
委托类型。注意,在事件的使用中委托不再被视为引用变量,而是被视为一种特殊的数据类型,所以声明事件也叫创建委托类型的变量。
- 具体来说,事件是委托类型的实例,它具有和委托类型相同的特性,包括可以添加或移除多个委托实例,触发委托实例等。
- 当我们声明一个事件时,实际上就是在声明一个委托类型的变量,然后使用关键字
event
修饰它,使得它只能在类内部被访问并且只能被用于绑定方法。
触发一个事件的通常做法是通过调用事件的委托变量来实现的。具体来说,事件触发器(包含触发语句的一个方法)将检查事件的委托变量是否为 null,如果不为 null,则调用委托变量。通常使用 Invoke
方法来调用委托变量。
例如,以下代码触发了 ButtonClick
事件:
ButtonClick?.Invoke(this, EventArgs.Empty);
其中:
Invoke()
是一个委托类型的方法,用于执行指定的委托。我们之前说的调用委托对象来调用其所引用的方法,底层就是使用了 Invoke()
方法。ButtonClick
事件的类型为 EventHandler
,它是一个委托类型,定义了接受两个参数(object sender, EventArgs e)
且返回类型为 void
的方法。因此:
this
表示将事件源作为参数传递给这些方法;EventArgs.Empty
表示将空的 EventArgs
对象传递给这些方法,即不传递附加信息。?.
是我们之前提到的可空运算符,表示当 ButtonClick
为 null 时不调用其 Invoke
方法。当然也并非一定要使用上述方法触发事件,也可以写成下面的格式:
if ( ChangeNum != null )
{
ButtonClick(this, EventArgs.Empty); // 事件被触发
}
else
{
Console.WriteLine( "event not fire" );
Console.ReadKey(); // 回车继续
}
事件处理器是一个方法,实现该方法时,需要遵循事件委托类型定义的方法签名,这里主要指该方法的返回类型、参数类型及顺序需要与委托类型定义的相同。
例如,以下代码实现了一个名为 OnButtonClick
的事件处理器:
// 事件处理器
private void OnButtonClick(object sender, EventArgs e)
{
// 处理事件
}
在事件处理器中,可以编写处理事件的代码。其中,sender
参数表示事件的来源,e
参数表示事件的附加信息。
在使用事件时,可以添加和移除事件处理器。
例如,以下代码通过 +=
运算符,添加了一个名为 OnButtonClick
的事件处理器:
ButtonClick += OnButtonClick;
这个代码将 OnButtonClick
方法添加到 ButtonClick
事件的事件处理器列表中。要从事件处理器列表中移除事件处理器,可以使用 -=
运算符:
ButtonClick -= OnButtonClick;
这个代码将 OnButtonClick
方法从 ButtonClick
事件的事件处理器列表中移除。
事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理器关联。包含事件的类用于发布事件,被称为 发布器(publisher) 类。其他接受该事件的类被称为 订阅器(subscriber) 类。事件使用 发布-订阅(publisher-subscriber) 模型。
发布器(publisher) 是一个包含事件(和委托)定义的对象。事件和委托之间的联系也定义在这个对象中。发布器(publisher)类的对象通过事件触发器调用这个事件,并通知其他的对象。
订阅器(subscriber) 是一个接受事件并提供事件处理器的对象。在发布器类中触发事件后,调用订阅器类中处理事件的方法。
下列代码演示了如何定义事件,如何订阅事件以及如何触发事件:
using System;
namespace EventDemo
{
// 定义一个委托类型,用于处理事件(也可以把它定义在发布器类中)
public delegate void EventHandler(object sender, EventArgs args);
// 定义一个发布器类,它会触发事件
public class Publisher
{
// 定义事件
public event EventHandler MyEvent;
// 触发事件的方法
public void RaiseEvent()
{
Console.WriteLine("事件被触发了!");
// 检查是否有订阅者( MyEvent是否为null),如果有则调用其事件处理器
MyEvent?.Invoke(this, EventArgs.Empty);
}
}
// 定义一个订阅器类,它会接收事件
public class Subscriber
{
// 事件处理方法,需要和委托类型定义的签名一致
public void HandleEvent(object sender, EventArgs args)
{
Console.WriteLine("事件被处理了!");
}
}
class Program
{
static void Main(string[] args)
{
// 创建一个发布器实例
Publisher pub = new Publisher();
// 创建一个订阅器实例
Subscriber sub = new Subscriber();
// 订阅事件
pub.MyEvent += sub.HandleEvent;
// 触发事件
pub.RaiseEvent();
Console.ReadKey();
}
}
}
上述代码中,判断事件的委托变量 MyEvent
是否为 null ,其实就是在判定创建该变量后,有没有通过 +=
订阅事件,为事件的委托变量添加可引用的事件处理器。同时因为事件在发布器内部是作为一个委托类型的变量被定义的,所以在类的外部要通过 实例名.事件名
来访问。
运行上面的代码会产生如下结果:
事件被触发了!
事件被处理了!
下面是一个简单的用于热水锅炉系统故障排除的应用程序,用于展示事件和委托的实际应用场景。当维修工程师检查锅炉时,锅炉的温度和压力会随着维修工程师的备注自动记录到日志文件中:
using System;
using System.IO;
namespace BoilerEventAppl
{
// 锅炉类
class Boiler
{
private int temp; // 锅炉温度
private int pressure; // 锅炉压力
// 构造函数,初始化温度和压力
public Boiler(int t, int p)
{
temp = t;
pressure = p;
}
// 获取锅炉温度
public int getTemp()
{
return temp;
}
// 获取锅炉压力
public int getPressure()
{
return pressure;
}
}
// 事件发布器类
class DelegateBoilerEvent
{
// 声明一个委托类型,用于指向处理 BoilerLog 事件的方法
public delegate void BoilerLogHandler(string status);
// 声明 BoilerLog 事件,事件类型为 BoilerLogHandler 委托
public event BoilerLogHandler BoilerEventLog;
// 发布 BoilerLog 事件,处理日志信息
public void LogProcess()
{
string remarks = "O. K";
Boiler b = new Boiler(100, 12);
int t = b.getTemp();
int p = b.getPressure();
// 判断锅炉温度和压力是否正常,如果不正常,需要进行维修
if (t > 150 || t < 80 || p < 12 || p > 15)
{
remarks = "Need Maintenance";
}
// 发布 BoilerLog 事件,传递日志信息
OnBoilerEventLog("Logging Info:\n");
OnBoilerEventLog("Temparature " + t + "\nPressure: " + p);
OnBoilerEventLog("\nMessage: " + remarks);
}
// 触发 BoilerLog 事件,调用事件委托
protected void OnBoilerEventLog(string message)
{
if (BoilerEventLog != null)
{
BoilerEventLog(message);
}
/* 也可以写成:BoilerEventLog?.Invoke(message); */
}
}
// 该类保留写入日志文件的条款
class BoilerInfoLogger
{
FileStream fs;
StreamWriter sw;
// 构造函数,打开日志文件
public BoilerInfoLogger(string filename)
{
fs = new FileStream(filename, FileMode.Append, FileAccess.Write);
sw = new StreamWriter(fs);
}
// 将日志信息写入文件
public void Logger(string info)
{
sw.WriteLine(info);
}
// 关闭日志文件
public void Close()
{
sw.Close();
fs.Close();
}
}
// 事件订阅器类
public class RecordBoilerInfo
{
// 定义一个处理 BoilerLog 事件的方法,输出日志信息到控制台
static void Logger(string info)
{
Console.WriteLine(info);
}
static void Main(string[] args)
{
// 创建一个文本日志记录器
BoilerInfoLogger filelog = new BoilerInfoLogger("e:\\boiler.txt");
// 创建一个委托发布器对象
DelegateBoilerEvent boilerEvent = new DelegateBoilerEvent();
// 向 BoilerLog 事件添加两个事件处理器,一个将日志信息输出到控制台,另一个将日志信息写入文件
boilerEvent.BoilerEventLog += new DelegateBoilerEvent.BoilerLogHandler(Logger);
boilerEvent.BoilerEventLog += new DelegateBoilerEvent.BoilerLogHandler(filelog.Logger);
// 触发事件并记录日志
boilerEvent.LogProcess();
// 等待用户按下回车键后关闭文件日志
Console.ReadLine();
filelog.Close();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
Logging info:
Temperature 100
Pressure 12
Message: O. K
在C#中,匿名方法是一种没有名称只有主体的方法。使用匿名方法可以避免显式定义方法的麻烦,同时提供了一种灵活的方式来传递代码块作为委托参数。在匿名方法中我们也不需要指定返回类型,它是从方法主体内的 return
语句推断的。
匿名方法的语法与普通方法的语法非常相似,但是没有方法名和返回类型。在 C# 中,匿名方法可以使用 delegate
关键字来定义,其一般形式如下:
delegate (parameter_list) { anonymous_method_body }
其中,parameter_list
指定了方法的参数列表,anonymous_method_body
指定了方法的主体。
匿名方法可以使用已经定义的委托类型进行声明,也可以使用 var
关键字自动推断委托类型,例如:
delegate int Calculator(int x, int y);
Calculator add = delegate(int x, int y) { return x + y; };
var multiply = delegate(int x, int y) { return x * y; };
这里我们使用了 Calculator
委托类型来声明了一个 add
方法和使用 var
关键字推断出的 multiply
方法。两个方法都是匿名方法,没有方法名,但是都具有参数列表和方法体。
在 C# 中,final 变量是指一旦赋值之后就不能再被修改的变量。在匿名方法中,final 变量被称为闭包变量。这是因为匿名方法可以访问其外部作用域中声明的变量,当匿名方法引用一个 final 变量时,它会创建一个闭包,以保存这个变量的引用。
当一个匿名方法引用了一个外部变量时,这个变量的生命周期将被延长,直到匿名方法执行完毕为止。这就是所谓的闭包行为。在匿名方法中,闭包变量是只读的,因为它们在方法执行期间不能被修改。
以下是一个使用闭包变量的匿名方法的示例:
class Program
{
static void Main(string[] args)
{
int x = 5;
Func<int, int> addX = delegate(int y) { return x + y; };
Console.WriteLine(addX(3)); // 输出 8
Console.WriteLine(addX(7)); // 输出 12
}
}
在这个示例中,我们创建了一个名为 addX
的匿名方法,它使用了外部的变量 x
。在匿名方法中,x
是一个闭包变量,因为它被匿名方法引用,并且它的值在匿名方法执行期间不能被修改。
需要注意的是,在匿名方法中使用的外部变量如果不是闭包变量,编译器将会报错。
我们在前面提到匿名方法可以作为委托的参数传递,其实就是指用匿名方法替代了委托类型实例化时需要将被引用的方法名作为参数传递给实例对象,例如:
class Program
{
delegate void Printer(string s);
static void Method(string s)
{
Console.WriteLine(s);
}
static void Main()
{
Printer p = delegate(string s) { Console.WriteLine(s); };
/* 可以替代以下代码:
Printer p = Method; 或
Printer p = new Printer(Method); */
p("Hello, world!");
}
}
这里我们定义了一个委托类型 Printer
,然后在 Main
方法中使用匿名方法创建了一个 p
委托对象,最后调用 p 方法输出了一个字符串。