Skip to main content
 Web开发网 » 站长学院 » 浏览器插件

面试 | .NET基础知识快速通关(8)

2021年10月11日5710百度已收录

面试 | .NET基础知识快速通关(8)  .net面试经验 第1张

【.NET】| 总结/Edison Zhou

此系列文章为我在2015年发布于博客园的.NET基础拾遗系列,它十分适合初中级.NET开发工程师在面试前进行一个系统的复习,因此我将其搬到头条上分享与你。

本文为第八篇,我们会对.NET的委托相关考点进行基础复习,全文会以Q/A的形式展现,即以面试题的形式来描述。

1 能说说委托的基本原理是啥吗?

委托这个概念对C++程序员来说并不陌生,因为它和C++中的函数指针非常类似,很多码农也喜欢称委托为安全的函数指针。无论这一说法是否正确,委托的的确确实现了和函数指针类似的功能,那就是提供了程序回调指定方法的机制。

在委托内部,包含了一个指向某个方法的指针(这一点上委托实现机制和C++的函数指针一致),为何称其为安全的呢?因为委托和其他.NET成员一样是一种类型,任何委托对象都是继承自System.Delegate的某个派生类的一个对象,下图展示了在.NET中委托的类结构:

面试 | .NET基础知识快速通关(8)  .net面试经验 第2张

从上图也可以看出,任何自定义的委托都继承自基类 System.Delegate,在这个类中,定义了大部分委托的特性。那么,下面可以看看在.NET中如何使用委托:

// 定义的一个委托public delegate void TestDelegate(int i);public class Program{ public static void Main(string[] args) { // 定义委托实例 TestDelegate td = new TestDelegate(PrintMessage); // 调用委托方法 td(0); td.Invoke(1); Console.ReadKey(); } public static void PrintMessage(int i) { Console.WriteLine("这是第{0}个方法!", i.ToString()); }}运行结果如下图所示:

面试 | .NET基础知识快速通关(8)  .net面试经验 第3张

上述代码中定义了一个名为TestDelegate的新类型,该类型直接继承自 System.MulticastDelegate,而且其中会包含一个名为Invoke、BeginInvoke和EndInvoke的方法,这些步骤都是由C#编译器自动帮我们完成的,可以通过Reflector验证一下如下图所示:

面试 | .NET基础知识快速通关(8)  .net面试经验 第4张

需要注意的是,委托既可以接受实例方法,也可以接受静态方法(如上述代码中接受的就是静态方法),其区别我们在1.2中详细道来。最后,委托被调用执行时,C#编译器可以接收一种简化程序员设计的语法,例如上述代码中的:td(1)。但是,本质上,委托的调用其实就是执行了在定义委托时所生成的Invoke方法。

2 委托回调静态方法 和 实例方法有什么区别?

首先,我们知道静态方法可以通过类名来访问而无需任何实例对象,当然在静态方法中也就不能访问类型中任何非静态成员。相反,实例方法则需要通过具体的实例对象来调用,可以访问实例对象中的任何成员。

其次,当一个实例方法被调用时,需要通过实例对象来访问,因此可以想象当绑定一个实例方法到委托时必须同时让委托得到实例方法的代码段和实例对象的信息,这样在委托被回调的时候.NET才能成功地执行该实例方法。

下图展示了委托内部的主要结构:

面试 | .NET基础知识快速通关(8)  .net面试经验 第5张

① _target是一个指向目标实例的引用,当绑定一个实例方法给委托时,该参数会作为一个指针指向该方法所在类型的一个实例对象。相反,当绑定一个静态方法时,该参数则被设置为null。

② _methodPtr则是一个指向绑定方法代码段的指针,这一点和C++的函数指针几乎一致。绑定静态方法或实例方法在这个成员的设置上并没有什么不同。

System.MulticastDelegate 在内部结构上相较System.Delegate增加了一个重要的成员变量:_prev,它用于指向委托链中的下一个委托,这也是实现多播委托的基石。

面试 | .NET基础知识快速通关(8)  .net面试经验 第6张

3 能说说什么是链式委托吗?

链式委托也被称为“多播委托”,其本质是一个由多个委托组成的链表。回顾上面1.2中的类结构,System.MulticastDelegate 类便是为链式委托而设计的。当两个及以上的委托被链接到一个委托链时,调用头部的委托将导致该链上的所有委托方法都被执行。

下面看看在.NET中,如何申明一个链式委托:

// 定义的一个委托public delegate void TestMulticastDelegate();public class Program{ public static void Main(string[] args) { // 申明委托并绑定第一个方法 TestMulticastDelegate tmd = new TestMulticastDelegate(PrintMessage1); // 绑定第二个方法 tmd += new TestMulticastDelegate(PrintMessage2); // 绑定第三个方法 tmd += new TestMulticastDelegate(PrintMessage3); // 调用委托 tmd(); Console.ReadKey(); } public static void PrintMessage1() { Console.WriteLine("调用第1个PrintMessage方法"); } public static void PrintMessage2() { Console.WriteLine("调用第2个PrintMessage方法"); } public static void PrintMessage3() { Console.WriteLine("调用第3个PrintMessage方法"); }}其运行结果如下图所示:

面试 | .NET基础知识快速通关(8)  .net面试经验 第7张

可以看到,调用头部的委托导致了所有委托方法的执行。通过前面的分析我们也可以知道:为委托+=增加方法以及为委托-=移除方法让我们看起来像是委托被修改了,其实它们并没有被修改。事实上,委托是恒定的。在为委托增加和移除方法时实际发生的是创建了一个新的委托,其调用列表是增加和移除后的方法结果。

面试 | .NET基础知识快速通关(8)  .net面试经验 第8张

另一方面,+= 或-= 这是一种简单明了的写法,回想在WindowsForm或者ASP.NET WebForms开发时,当添加一个按钮事件,VS便会自动为我们生成类似的代码,这样一想是不是又很熟悉了。

现在,我们再用一种更简单明了的方法来写:

TestMulticastDelegate tmd = PrintMessage1;tmd += PrintMessage2;tmd += PrintMessage3;tmd();其执行结果与上图一致,只不过C#编译器的智能化已经可以帮我们省略了很多代码。

最后,我们要用一种比较复杂的方法来写,但是却是链式委托的核心所在:

TestMulticastDelegate tmd1 = new  TestMulticastDelegate(PrintMessage1);TestMulticastDelegate tmd2 = new TestMulticastDelegate(PrintMessage2);TestMulticastDelegate tmd3 = new  TestMulticastDelegate(PrintMessage3);// 核心本质:将三个委托串联起来TestMulticastDelegate tmd = tmd1 + tmd2 + tmd3;tmd.Invoke();我们在实际开发中经常使用第二种方法,但是却不能不了解方法三,它是链式委托的本质所在。

4 链式委托的执行顺序是如何形成的?

前面我们已经知道链式委托的基本特性就是一个以委托组成的链表,而当委托链上任何一个委托方法被调用时,其后面的所有委托方法都将会被依次地顺序调用。那么问题来了,委托链上的顺序是如何形成的?这里回顾一下上面1.3中的示例代码,通过Reflector 反编译 一下看看,一探究竟:

面试 | .NET基础知识快速通关(8)  .net面试经验 第9张

从编译后的结果可以看到,+=的本质又是调用了Delegate.Combine方法,该方法将两个委托链接起来,并且把第一个委托放在第二个委托之前,因此可以将两个委托的相加理解为Deletegate.Combine(Delegate a,Delegate b)的调用。我们可以再次回顾System.MulticastDelegate的类结构:

面试 | .NET基础知识快速通关(8)  .net面试经验 第10张

其中_prev成员是一个指向下一个委托成员的指针,当某个委托被链接到当前委托的后面时,该成员会被设置为指向那个后续的委托实例。.NET也是依靠这一个引用来逐一找到当前委托的所有后续委托并以此执行方法。

那么,问题又来了?程序员是否能够有能力控制链式委托的执行顺序呢?也许我们会说,只要在定义时按照需求希望的顺序来依次添加就可以了。但是,如果要在定义完成之后突然希望改变执行顺序呢?又或者,程序需要按照实际的运行情况再来决定链式委托的执行顺序呢?

接下来就是见证奇迹的时刻:

// 申明委托并绑定第一个方法TestMulticastDelegate tmd = new TestMulticastDelegate(PrintMessage1);// 绑定第二个方法tmd += new TestMulticastDelegate(PrintMessage2);// 绑定第三个方法tmd += new TestMulticastDelegate(PrintMessage3);// 获取所有委托方法Delegate[] dels = tmd.GetInvocationList();上述代码调用了定义在 System.MulticastDelegate 中的 GetInvocationList() 方法,用以获得整个链式委托中的所有委托。接下来,我们就可以按照我们所希望的顺序去执行它们。

5 如何定义有返回值方法的委托链?

委托的方法既可以是无返回值的,也可以是有返回值的,但如果多一个带返回值的方法被添加到委托链中时,我们需要手动地调用委托链上的每个方法,否则只能得到委托链上最后被调用的方法的返回值。

为了验证结论,我们可以通过如下代码进行演示:

// 定义一个委托public delegate string GetStringDelegate();class Program{ static void Main(string[] args) { // GetSelfDefinedString方法被最后添加 GetStringDelegate myDelegate1 = GetDateTimeString; myDelegate1 += GetTypeNameString; myDelegate1 += GetSelfDefinedString; Console.WriteLine(myDelegate1()); Console.WriteLine(); // GetDateTimeString方法被最后添加 GetStringDelegate myDelegate2 = GetSelfDefinedString; myDelegate2 += GetTypeNameString; myDelegate2 += GetDateTimeString; Console.WriteLine(myDelegate2()); Console.WriteLine(); // GetTypeNameString方法被最后添加 GetStringDelegate myDelegate3 = GetSelfDefinedString; myDelegate3 += GetDateTimeString; myDelegate3 += GetTypeNameString; Console.WriteLine(myDelegate3()); Console.ReadKey(); } static string GetDateTimeString() { return DateTime.Now.ToString(); } static string GetTypeNameString() { return typeof(Program).ToString(); } static string GetSelfDefinedString() { string result = "我是一个字符串!"; return result; }}其运行结果如下图所示:

面试 | .NET基础知识快速通关(8)  .net面试经验 第11张

从上图可以看到,虽然委托链中的所有方法都被正确执行,但是我们只得到了最后一个方法的返回值。在这种情况下,我们应该如何得到所有方法的返回值呢?回顾刚刚提到的GetInvocationList()方法,我们可以利用它来手动地执行委托链中的每个方法。

GetStringDelegate myDelegate1 = GetDateTimeString;myDelegate1 += GetTypeNameString;myDelegate1 += GetSelfDefinedString;foreach (var del in myDelegate1.GetInvocationList()){ Console.WriteLine(del.DynamicInvoke());}通过上述代码,委托链中每个方法的返回值都不会丢失,下图是执行结果:

面试 | .NET基础知识快速通关(8)  .net面试经验 第12张

6 委托都有哪些可以应用的场合?

委托的功能和其名字非常类似,在设计中其思想在于将工作委派给其他特定的类型、组件、方法或程序集。委托的使用者可以理解为工作的分派者,在通常情况下使用者清楚地知道哪些工作需要执行、执行的结果又是什么,但是他不会亲自地去做这些工作,而是恰当地把这些工作分派出去。

这里,我们假设要写一个日志子系统,该子系统的需求是使用者希望的都是一个单一的方法传入日志内容和日志类型,而日志子系统会根据具体情况来进行写日志的动作。对于日志子系统的设计者来说,写一条日志可能需要包含一系列的工作,而日志子系统决定把这些工作进行适当的分派,这时就需要使用一个委托成员。

下面的代码展示了该日志子系统的简单实现方式:

① 定义枚举:日志的类别

public enum LogType{ Debug, Trace, Info, Warn, Error}② 定义委托,由日志使用者直接执行来完成写日志的工作

public delegate void Log(string content, LogType type);③ 定义日志管理类,在构造方法中为记录日志委托定义了默认的逻辑(这里采用了部分类的书写,将各部分的委托方法分隔开,便于理解)

public sealed partial class LogManager:IDisposable{ private Type _componentType; private String _logfile; private FileStream _fs; public Log WriteLog; //用来写日志的委托 //锁 private static object mutext = new object(); //严格控制无参的构造方法 private LogManager() { WriteLog = new Log(PrepareLogFile); WriteLog += OpenStream; //打开流 WriteLog += AppendLocalTime; //添加本地时间 WriteLog += AppendSeperator; //添加分隔符 WriteLog += AppendComponentType;//添加模块类别 WriteLog += AppendSeperator; //添加分隔符 WriteLog += AppendType; //添加日志类别 WriteLog += AppendSeperator; //添加分隔符 WriteLog += AppendContent; //添加内容 WriteLog += AppendNewLine; //添加回车 WriteLog += CloseStream; //关闭流 } /// <summary> /// 构造方法 /// </summary> /// <param name="type">使用该日志的类型</param> /// <param name="file">日志文件全路径</param> public LogManager(Type type, String file):this() { _logfile = file; _componentType = type; } /// <summary> /// 释放FileStream对象 /// </summary> public void Dispose() { if (_fs != null) _fs.Dispose(); GC.SuppressFinalize(this); } ~LogManager() { if (_fs != null) _fs.Dispose(); }}/// <summary>/// 委托链上的方法(和日志文件有关的操作)/// </summary>public sealed partial class LogManager:IDisposable{ /// <summary> /// 如果日志文件不存在,则新建日志文件 /// </summary> private void PrepareLogFile(String content, LogType type) { //只允许单线程创建日志文件 lock(mutext) { if (!File.Exists(_logfile)) using (FileStream fs = File.Create(_logfile)) { } } } /// <summary> /// 打开文件流 /// </summary> private void OpenStream(String content, LogType type) { _fs = File.Open(_logfile, FileMode.Append); } /// <summary> /// 关闭文件流 /// </summary> private void CloseStream(String content, LogType type) { _fs.Close(); _fs.Dispose(); }}/// <summary>/// 委托链上的方法(和日志时间有关的操作)/// </summary>public sealed partial class LogManager : IDisposable{ /// <summary> /// 为日志添加当前UTC时间 /// </summary> private void AppendUTCTime(String content, LogType type) { String time=DateTime.Now.ToUniversalTime().ToString(); Byte[] con = Encoding.Default.GetBytes(time); _fs.Write(con, 0, con.Length); } /// <summary> /// 为日志添加本地时间 /// </summary> private void AppendLocalTime(String content, LogType type) { String time = DateTime.Now.ToLocalTime().ToString(); Byte[] con = Encoding.Default.GetBytes(time); _fs.Write(con, 0, con.Length); }}/// <summary>/// 委托链上的方法(和日志内容有关的操作)/// </summary>public sealed partial class LogManager : IDisposable{ /// <summary> /// 添加日志内容 /// </summary> private void AppendContent(String content, LogType type) { Byte[] con = Encoding.Default.GetBytes(content); _fs.Write(con, 0, con.Length); } /// <summary> /// 为日志添加组件类型 /// </summary> private void AppendComponentType(String content, LogType type) { Byte[] con = Encoding.Default.GetBytes(_componentType.ToString()); _fs.Write(con, 0, con.Length); } /// <summary> /// 添加日志类型 /// </summary> private void AppendType(String content, LogType type) { String typestring = String.Empty; switch (type) { case LogType.Debug: typestring = "Debug"; break; case LogType.Error: typestring = "Error"; break; case LogType.Info: typestring = "Info"; break; case LogType.Trace: typestring = "Trace"; break; case LogType.Warn: typestring = "Warn"; break; default: typestring = ""; break; } Byte[] con = Encoding.Default.GetBytes(typestring); _fs.Write(con, 0, con.Length); }}/// <summary>/// 委托链上的方法(和日志的格式控制有关的操作)/// </summary>public sealed partial class LogManager : IDisposable{ /// <summary> /// 添加分隔符 /// </summary> private void AppendSeperator(String content, LogType type) { Byte[] con = Encoding.Default.GetBytes(" | "); _fs.Write(con, 0, con.Length); } /// <summary> /// 添加换行符 /// </summary> private void AppendNewLine(String content, LogType type) { Byte[] con = Encoding.Default.GetBytes("\r\n"); _fs.Write(con, 0, con.Length); }}/// <summary>/// 修改所使用的时间类型/// </summary>public sealed partial class LogManager : IDisposable{ /// <summary> /// 设置使用UTC时间 /// </summary> public void UseUTCTime() { WriteLog = new Log(PrepareLogFile); WriteLog += OpenStream; WriteLog += AppendUTCTime; WriteLog += AppendSeperator; WriteLog += AppendComponentType; WriteLog += AppendSeperator; WriteLog += AppendType; WriteLog += AppendSeperator; WriteLog += AppendContent; WriteLog += AppendNewLine; WriteLog += CloseStream; } /// <summary> /// 设置使用本地时间 /// </summary> public void UseLocalTime() { WriteLog = new Log(PrepareLogFile); WriteLog += OpenStream; WriteLog += AppendLocalTime; WriteLog += AppendSeperator; WriteLog += AppendComponentType; WriteLog += AppendSeperator; WriteLog += AppendType; WriteLog += AppendSeperator; WriteLog += AppendContent; WriteLog += AppendNewLine; WriteLog += CloseStream; }}日志管理类定义了一些列符合Log委托的方法,这些方法可以被添加到记录日志的委托对象之中,以构成整个日志记录的动作。在日后的扩展中,主要的工作也集中在添加新的符合Log委托定义的方法,并且将其添加到委托链上。

④ 在Main方法中调用LogManager的Log委托实例来写日志,LogManager只需要管理这个委托,负责分派任务即可。

public class Program{ public static void Main(string[] args) { //使用日志 using (LogManager logmanager = new LogManager(Type.GetType("LogSystem.Program"), "D:\\TestLog.txt")) { logmanager.WriteLog("新建了日志", LogType.Debug); logmanager.WriteLog("写数据", LogType.Debug); logmanager.UseUTCTime(); logmanager.WriteLog("现在是UTC时间", LogType.Debug); logmanager.UseLocalTime(); logmanager.WriteLog("回到本地时间", LogType.Debug); logmanager.WriteLog("发生错误", LogType.Error); logmanager.WriteLog("准备退出", LogType.Info); } Console.ReadKey(); }}代码中初始化委托成员的过程既是任务分派的过程,可以注意到LogManager的UseUTCTime和UseLocalTime方法都是被委托成员进行了重新的分配,也可以理解为任务的再分配。

下图是上述代码的执行结果,将日志信息写入了D:\TestLog.txt中:

面试 | .NET基础知识快速通关(8)  .net面试经验 第13张

总结

本文总结复习了.NET的委托相关的重要知识点,下一篇会总结.NET中事件相关的重要知识点,欢迎继续关注!

参考资料(全是经典)

朱毅,《进入IT企业必读的200个.NET面试题》

张子阳,《.NET之美:.NET关键技术深入解析》

王涛,《你必须知道的.NET(第二版)》

评论列表暂无评论
发表评论
微信