一、委托的语言设计目标:
1、团队想要拥有可用于任何后期绑定算法的公共语言构造。委托促使开发人员学习一个概念,并在许多不同的软件问题中使用这同一概念。
2、该团队希望支持单一和多播方法调用。(多播委托是将多个方法调用链接在一起的委托。)
3、团队想要委托子啊C#构造中支持开发人员所预期的相同的类型安全性。
4、团队认识到事件模式是一个特定模式,委托或任何后期绑定算法在这种模式下都很有用。团队需要确保委托的代码可以为.NET事件模式提供基础。
二、知识点目录
1、delegate关键字和它所生成的代码。
2、System.Delegate类中的功能,以及如何使用这些功能。
3、如何创建类型安全的委托。
4、如何创建可通过委托调用的方法。
5、如何使用Lambda表达式来处理委托和事件。
6、作为LINQ的构建基块的委托的用途。
7、委托如何成为.NET事件模式的基础,以及委托和事件之间的区别。
三、System.Delegate和delegate关键字
1、通过“delegate”关键字来定义委托,示例
//From the .NET Core library
//Define the delegate type:
public delegate int Comparison<in T>(T left,T right);
此委托,编译器会生成一个类,它派生自与使用的签名匹配的System.Delegate(在此例中,是返回一个整数并具有两个参数的方法。)该委托的类型是Comparison。Comparison委托类型是泛型类型。
2、声明委托的示例:
定义委托之后,可以创建该类型的示例。与C#中的所有变量一样,不能直接在命名空间中或全局命名空间中声明委托实例。
//inside a class definition
//Declare an instance of that type:
public Comparison<T> comparator;
变量的类型是Comparison<T>(前面定义的委托类型)。变量的名称是comparator.
上面的代码片段在类中声明了一个成员变量。还可以声明作为局部变量或方法参数的委托变量。
3、调用委托:
可通过调用某个委托来调用处于该委托列表中的方法。在Sort()方法内部,代码会调用比较方法以确定放置对象的顺序:
int result= comparator(left,right);
在上面的行中,代码会调用附加到委托的方法。可将变量视为方法名称,并使用普通方法调用语法调用它。
4、分配、添加和删除调用目标:
这是委托类型的定义方式,以及声明和调用委托实例的方式。
要使用List.Sort()方法的开发人员需要定义签名与委托类型定义匹配的方法,并将它分配给排序方法使用的委托。此分配会将方法添加到该委托对象的调用列表。
假设要按长度对字符串列表进行排序。比较函数可能如下所示:
private static int CompareLength(string left,string right)=>
          left.Length.CompareTo(right,Length);
方法声明为私用方法。这没有什么不对。你可能不希望此方法是公共接口的一部分。它仍可以在附加到委托时用作比较方法。调用代码会将此方法附加到委托对象的目标列表,并且可以通过该委托访问它。
通过将该方法传递给List.Sort()方法来创建该关系:
phrases.Sort(CompareLength);
请注意,在不带括号的情况下使用方法名称。将方法用作参数会告知编译器嫁给你方法引用转换为可以用作委托调用目标的引用,并将该方法作为调用目标进行附加。
还可以通过声明“Comparison<string>"类型的标量并进行分配来显式执行操作:
Comparison<string> comparer=CompareLength;
phrases.Sort(comparer);
在用作委托目标的方法是小型方法的用法中,经常使用lambda表达式语法来执行分配:
Comparison<string> comparer=(left,right)=>left.Length.CompareTo(ritht.Length);
phrases.Sort(comparer);
5、委托和MulticastDelegate类:
上面介绍的语言支持可提供在使用委托时通常需要的功能和支持。这些功能采用.NET Core Framework中的两个类进行构建:Delegate和MulticastDelegate。
System.Delegate类及其单个直接子类System.MulticasDelegate可提供框架支持,以便创建委托、将方法注册为委托目标以及调用注册为委托目标的所有方法。
四、强类型委托:
1、.NET Core 框架包含几个在需要委托类型时可重用的类型。这些事泛型定义,因此需要新的方法声明时可以声明自定义。
第一个类型是Action类型和一些变体:
public delegate void Action();
public delegate void Action<in T> (T arg);
public delegate void Action<in T1,in T2>(T1 arg1,T2 arg2);
//Other variations removed for brevity.
有关协方差的文章中介绍了泛型类型参数的in修饰符。
Action委托的变体可包换多达16个参数。重要的是这些定义对每个委托参数使用不同的泛型参数:这样可以具有最大的灵活性。方法参数不需要但可能是相同的类型。
对任何具有void返回类型的委托类型是哟个一种Action类型。
此框架还包括几种可用于返回值的委托类型的泛型委托类型:
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T1,out TResult>(T1 arg);
pulbic delegate TResult Func<in T1,in T2,out TResult>(T1 arg1,T2 arg2);
Func委托的变体可包含多达16个输入参数。按照约定,结果的类型始终是所有Func声明中的最后一个类型参数。
对任何返回值的委托类型使用一种Func类型。
还有一种专门的委托类型Predicate<T>,此类型返回单个值得测试结果:
public delegate bool Predicate<in T>(T obj);
PS:对于任何Predicate类型,均存在一个在结构上等效得Func类型,例如:
Func<string ,bool>TestForString;
Predicate<string>AnotherTestForString;
这两种类型不是等效得,这两个变量不能壶关使用。
五、委托得常见模式:
1、本章内容:
  • 通过委托生成自己得组件
  • 首次实现
  • 设置输出格式
  • 生成第二个输出引擎
  • 显示另外2个
     委托提供了一种机制,可实现涉及组件间最小耦合度软件涉及。
此类
2、通过委托生成自己的组件:
    定义一个可用于大型系统中日志信息的组件。库组件可以在多种不同的环境中和多个不同的平台上使用。管理日志的组件中有很多常用功能。它需要接受来自系统中任何组件的消息。这些消息将具有不同的优先级(核心组件可进行管理)。消息应当具有其最终存档形式的时间戳。对于更高级的方案,你可以按源组件筛选消息。
    此功能会有一个方面会经常发生变化:写入消息的位置。在某些环境中。它们可能会写入到错误控制台。在其他环境中,可能会写入一个文件。其他可能性包括数据库存储、操作系统事件日志或其他文档存储。
    还有可能用于不同方案的输出组合。建议你将消息写入控制台和文件。
    基于委托的设计将提供极大的灵活性,从而轻松支持可能在以后添加的存储机制。
    基于此设计,主日志组件可以是非虚拟,甚至是密封的类。你可以插入任何委托集,将消息写入不同的存储介质。对多播委托的内置支持有助于支持必须将消息写入多个位置(文件和控制台)的情况。
3、首次实现:
    我们从小处着手:初始实现会接受新消息并使用任意附加委托编写它们。你可以从一个将消息写入控制台的委托开始。
public static class Logger
{
    public static Action<string>?WriteMessage;
 
    public static void LogMessage(string msg)
     {
          if(WriteMessage is not null)
                WriteMessage(msg);
     }
}
上面的静态类是可以发挥作用的最简单的类。我们需要编写将消息写入控制台的方法的单个实现:
public static class LoggingMethods
{
    public static void LogToConsole(string message)
     {
          Console.Error.WriteLine(message);
     }
}
最后,你需要通过将委托附加到记录器中声明的WriteMessage委托来进行挂钩:
Logger.WriteMessage+=LoggingMethods.LogToConsole;
  实践:
  到目前为止,我们的示例都相当简单,但仍演示了一些关于委托设计的重要指南。
  借助在核心框架中定义的委托类型,用户可更轻松地使用委托。无需定义新类型,而且使用你库的开发者不需要学习新的专用委托类型。
  使用的接口尽可能小且灵活:若要创建新的输出记录器,必须创建一个方法。该方法可以是静态方法或实例方法。它可能具有任何访问权限。
4、设置输出格式
  让第一个版本更可靠,然后开始创建其他日志记录机制。
  然后,向LogMessage()方法添加一些参数,以便日志类创建更多结构化消息:
public enum Severity
{
  Verbose,
  Trace,
  Information,
  Waring,
  Error,
  Critical    
}
public static class Logger
{
  public static Action<string>?WriteMessage;
  
  public static void LogMessage(Severity s,string component,string msg)
  {
     var outputMsg=$"{DateTime.Now}\t{s}\t{component}\t{msg}" ;
     if(WriteMessage is not null)
        WriteMessage(outputMsg); 
  }  
}

  接下来,使用Severity参数来筛选发送到日志输出的消息。

  public static class Logger
  {
      public static Action<string>  WriteMessage;

      public static Severity LogLevel { get; set; } = Severity.Warning;

      public static void LogMessage(Severity s, string component, string msg)
      {
          if (s < LogLevel)
          {
              return;
          }
          var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
          if (WriteMessage != null)
              WriteMessage(outputMsg);
      }
  }

  实践:

  已向日志记录基础结构添加了新功能。由于记录器组件极其松散地耦合到输出机制,因此可在不影响代码实现记录器托管的情况下添加新功能。

  继续构建,你会看到更多的示例,其中显示这种松散的耦合度在更新站点部件方面实现了很高的灵活性,而不会对其他位置做出更改。实际上,在更大的应用程序中,记录器输出类可能位于不同的程序集中,甚至不需要重新生成。

5、生成第二个输出引擎:

  还将附带日志组件。我们再添加一个将消息记录到文件的输出引擎。这将是一个更为普及的输出引擎。它将是一个封装文件操作的类,并确保文件在每次写入后始终处于关闭状态。这可以确保生成每条消息后将所有数据刷新到磁盘。

  下面是基于文件的记录器:

 public class FileLogger
 {
     private readonly string logPath;
     public FileLogger(string path)
     {
         logPath = path;
         Logger.WriteMessage += LogMessage;
     }
     public void DetachLog() => Logger.WriteMessage -= LogMessage;
     //make sure this can't throw.
     private void LogMessage(string msg)
     {
         try
         {
             using(var log = File.AppendText(logPath))
             {
                 log.WriteLine(msg);
                 log.Flush();
             }
         }
         catch (Exception)
         {
             //Hmm . We caught an exception while
             //logging . We can't really log the 
             //problem (since it's the log that's failing).
             //So , while normally, catching an exception
             //and doing nothing isn't wise,it's really the
             //only reasonable option here.
             throw;
         }
     }
 }

创建此类后,可将它实例化,然后它会将LogMessage方法附加到记录器组件中:

var file = new FileLogger("log.txt");

这两项并不互相排斥。你可以附加这两种日志方法并生成要发送到控制台和文件的消息:

var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier

以后,即使在同一个应用程序中,也可在不对系统产生任何其他问题的情况下删除其中一个委托

Logger.WriteMessage -= LoggingMethods.LogToConsole;

  实践:

现在,你已添加日志记录子系统的第二个输出处理程序。 这需要更多的基础结构来正确支持文件系统。 此委托为实例方法。 其为私有方法。 由于委托基础结构可以连接委托,因此不需要太高的可访问性。

其次,基于委托的设计可实现多种输出方法,且无需额外的代码。 无需生成任何其他基础结构来支持多种输出方法。 它们将变为调用列表上的另一种方法。

需要特别注意文件日志记录输出方法中的代码。 对其进行编码以确保不引发任何异常。 虽然不是绝对必要,但这通常是很好的做法。 如果任意一种委托方法引发异常,将不会调用该调用中剩余的其他委托。

最后请注意,文件记录器必须通过打开和关闭每条日志消息上的文件来管理其资源。 可以选择让文件保持打开状态,并在完成后执行 IDisposable 以关闭文件。 这两种方法各有利弊。 两者都在类之间创建了更高的耦合度。

为了支持这两种方案,Logger 类中的代码都不需要更新

6、处理NULL委托

  最后,更新LogMessage方法,从而在没有选择输出机制的情况下更加可靠。WriteMessage委托没有附加调用列表时,当前实现将引发NullReferenceException。你可能更需要在没有附加方法时自行继续的设计。将null条件运算符与Delegate.Invoke()方法结合使用时,很容易实现该目标:

public static void LogMessage(string msg)
{
    WriteMessage?.Invoke(msg);
}

当左操作数(本例中为WriteMessage)为null时,null条件运算符(?.)会短路,这意味着不会尝试记录消息。

不会在System.Delegate或System.MulticasDelegate的文档中列出Invoke()方法。编译器将为声明的所有委托类型生成类型安全的Invoke方法。在此示例中,这意味着Invoke只需要一个string参数,并且有一个无效返回类型。

六、事件介绍:

  1、事件的简介:

  和委托类似,事件时后期绑定机制。实际上,事件时建立在对委托的语言支持之上的。

  事件是对象用于(向系统中的所有相关组件)广播已发生的一种方式。任何其他组件都可以订阅事件,并在事件引发时得到通知。

  你可能已在某些编程中使用过事件。许多图形系统都具有用于报告用户交互的事件模型。这些事件会报告鼠标移动、按钮点击和类似的交互。这是使用事件最常见情景之一,但并非唯一的情景。
  可以定义应针对类引发的事件。使用事件时,需要注意的一点是特定事件可能没有任何注册的对象。必须编写代码,以确保在未配置侦听器时不会引发事件。

  通过订阅事件,还可在两个对象(事件源和事件接收器)之间创建耦合。需要确保当不再对事件感兴趣时,事件接收器将从事件源取消订阅。

  2、事件支持的设计目标

  事件的语言设计针对这些目标:

  •   在事件源和事件接收器之间启用非常小的耦合。这两个组件可能不会由同一个组织编写,甚至可能会通过完全不同的计划进行更新。
  •   订阅事件并从同一事件取消订阅应该非常简单。
  •   事件源应支持多个事件订阅服务器。它还应支持不附加任何事件订阅服务器。

  3、事件的语言支持:

  用于定义事件以及订阅或取消订阅事件的语法是对委托语法的扩展。

  定义使用event关键字的事件:

public event EventHandler<FileListArgs> Progress;

  该事件(在此示例中,为EventHandler<FileListArgs>)的类型必须为委托类型。声明事件时,应遵循许多约定。通常情况下,事件委托类型具有无效的返回。事件声明应为谓词或谓词短语。当事件报告已发生的事情时,请使用过去时。使用现在时谓词(例如Closing)报告将要发生的事情。通常,使用现在时表示类支持某种类型的自定义行为。最常见的方案之一是支持取消。例如,Closing事件可能包括指示是否应继续执行关闭操作的参数。其他方案可能会允许调用方通过更新事件参数的属性来修改行为。你可以引发一个事件以指示算法将采取的建议的下一步操作。事件处理程序可以通过修改事件参数的属性授权不同的操作。

  想要引发事件时,使用委托调用语法调用事件处理程序:

Progress?.Invoke(this, new FileListArgs(file));

  如委托部分中所介绍的那样,?.运算符可以轻松确保在事件没有订阅服务器时不引发事件。

  通过使用+=运算符订阅事件:

EventHandler<FileListArgs> onProgress = (sender, eventArgs) =>
    Console.WriteLine(eventArgs.FoundFile);
fileLister.Progress += onProgress;

  处理程序方法通常前缀”On“,后跟事件名称,如上所示。

  使用-=运算符取消订阅:

fileLister.Progress -= onProgress;

请务必为表示事件处理程序的表达式声明局部变量。 这将确保取消订阅删除该处理程序。 如果使用的是 lambda 表达式的主体,则将尝试删除从未附加过的处理程序,此操作为无效操作

七、事件和委托的区分:

对不熟悉 .NET Core 平台的开发人员而言,在基于 delegates 的设计和基于 events 的设计之间做出选择是困难的。 委托或事件的选择通常比较难,因为这两种语言功能很相似。 事件甚至是使用委托的语言支持构建的。

它们都提供了一个后期绑定方案:在该方案中,组件通过调用仅在运行时识别的方法进行通信。 它们都支持单个和多个订阅服务器方法。 这称为单播和多播支持。 二者均支持用于添加和删除处理程序的类似语法。 最后,引发事件和调用委托使用完全相同的方法调用语法。 它们甚至都支持与 ?. 运算符一起使用的相同的 Invoke() 方法语法。

鉴于所有这些相似之处,很难确定何时使用何种语法。

侦听事件是可选的

在确定要使用的语言功能时,最重要的考虑因素为是否必须具有附加的订阅服务器。 如果代码必须调用订阅服务器提供的代码,则在需要实现回调时,应使用基于委托的设计。 如果你的代码在不调用任何订阅服务器的情况下可完成其所有工作,则应使用基于事件的设计。

请考虑本部分中生成的示例。 必须为使用 List.Sort() 生成的代码提供 comparer 函数,以便对元素进行正确排序。 必须与委托一起提供 LINQ 查询,以便确定要返回的元素。 二者均使用与委托一起生成的设计。

请考虑 Progress 事件。 它会报告任务进度。 无论是否具有侦听器,该任务将继续进行。 FileSearcher 是另一个示例。 即使没有附加事件订阅服务器,它仍将搜索和查找已找到的所有文件。 即使没有任何订阅服务器侦听事件,UX 控件仍正常工作。 它们都使用基于事件的设计。

返回值需要委托

另一个注意事项是委托方法所需的方法原型。 如你所见,用于事件的委托均具有无效的返回类型。 你还看到,存在创建事件处理程序的惯用语,该事件处理程序通过修改事件参数对象的属性将信息传回到事件源。 虽然这些惯用语可发挥作用,但它们不像从方法返回值那样自然。

请注意,这两种试探法可能经常同时存在:如果委托方法返回值,则可能会以某种方式影响算法。

事件具有专用调用

包含事件的类以外的类只能添加和删除事件侦听器;只有包含事件的类才能调用事件。 事件通常是公共类成员。 相比之下,委托通常作为参数传递,并存储为私有类成员(如果它们全部存储)。

事件侦听器通常具有较长的生存期

事件侦听器通常具有较长的生存期的这一理由不太充分。 但是,你可能会发现,当事件源将在很长一段时间内引发事件时,基于事件的设计会更加自然。 可以在许多系统上看到基于事件的 UX 控件设计示例。 订阅事件后,事件源可能会在程序的整个生存期内引发事件。 (当不再需要事件时,可以取消订阅事件。)

将其与许多基于委托的设计(其中委托用作方法的参数,且在返回该方法后不再使用此委托)进行比较。

仔细评估

以上考虑因素并非固定不变的规则。 相反,它们代表可帮助决定针对特定使用情况的最佳选择的指南。 因为两者类似,所以甚至可以将两者作为原型,并考虑使用更加自然的一种。 两者均能很好地处理后期绑定方案。 使用能与设计进行最佳通讯的一种。

委托和事件 - C# | Microsoft Learn