事件和委托与调用方法

本文关键字:调用 方法 事件 | 更新日期: 2024-10-31 09:55:23

我希望这个问题不要与他人密切相关,但其他人似乎不能填补知识上的空白。

这似乎是尝试理解事件和代表的热门话题,在阅读了许多 SO 问题和 MSDN 文章后,我不敢说我仍然不明白。在创建了几年出色的Web应用程序之后,我发现自己因不理解它们而感到非常沮丧。请任何人都可以在通用代码中澄清这一点。所以问题是,为什么要使用事件和委托而不是调用方法?

以下是我在工作中编写的一些基本代码。我能够利用活动和委托吗?

Public Class Email
{
   public string To {get;set;}
   //Omitted code
   public void Send()
   {
    //Omitted code that sends.
   }
}
Public Class SomeClass
{
   //Some props
   Public void DoWork()
  {
    //Code that does some magic
    //Now Send Email
    Email newEmail = new Email();
    newEmail.To = "me@me.com";
    newEmail.Send();
  }
}

这可能不是最好的例子,但无论如何DoWork()方法可以订阅电子邮件吗?这行得通吗?任何帮助我真正了解事件和代表将不胜感激。

问候

事件和委托与调用方法

我在实际编程中发现使用事件和委托的最大原因是简化代码维护任务并鼓励代码重用

当一个类调用另一个类中的方法时,这些类是"紧密耦合的"。 紧密耦合的类越多,在不更改其他几个类的情况下更改其中一个类的难度就越大。 那时你还不如写一个大类。

相反,使用事件使事情更加"松散耦合",并且可以更轻松地更改一个类而不必打扰其他类。

以你上面的例子为例,假设我们有第三个类,Logger,它应该在发送电子邮件时记录。 它使用一种方法 LogEvent(string desc, DateTime time) 将条目写入日志:

public class Logger 
{
  ...
  public void LogEvent(string desc, DateTime time)
  {
    ...//some sort of logging happens here
  }
}

如果我们使用方法,我们需要更新Email类的Send方法来实例化Logger并调用其LogEvent方法:

public void Send()
   {
    //Omitted code that sends.
    var logger = new Logger();
    logger.LogEvent("Sent message", DateTime.Now);
   }

现在EmailLogger紧密耦合。 如果我们在 Logger 中更改该LogEvent方法的签名 ,我们还必须对 Email 进行更改。 您是否看到当您处理即使是中型项目时,这会很快成为一场噩梦? 此外,甚至没有人愿意尝试使用LogEvent方法,因为他们知道,如果他们需要对其进行任何更改,他们将不得不开始更改其他课程,而本应是一个下午的工作很快就变成了一周。 因此,相反,他们编写了一个新方法或一个新类,然后与他们正在做的事情紧密耦合,事情变得臃肿,每个程序员开始进入他们自己的代码的小"贫民窟"。 当你必须稍后进来并弄清楚程序到底在做什么或寻找错误时,这是非常非常糟糕的。

如果改为将一些事件放在Email类上,则可以松散地耦合这些类:

Public Class Email
{
   public event EventHandler<EventArgs> Sent;
   private void OnSent(EventArgs e)
    {
        if (Sent!= null)
            Sent(this, e);
    }
   public string To {get;set;}
   //Omitted code
   public void Send()
   {
    //Omitted code that sends.
    OnSent(new EventArgs());//raise the event
   }
}

现在,您可以将事件处理程序添加到Logger,并从应用程序中的几乎任何位置将其订阅到 Email.Sent 事件,并让它执行需要执行的操作:

public class Logger 
{
  ...
  public void Email_OnSent(object sender, EventArgs e)
  {
    LogEvent("Message Sent", DateTime.Now);
  }
  public void LogEvent(string desc, DateTime time)
  {
    ...//some sort of logging happens here
  }
}

和其他地方:

var logger = new Logger();
var email = new Email();
email.Sent += logger.Email_OnSent;//subscribe to the event

现在,您的类非常松散地耦合,六个月后,当您决定希望Logger捕获更多或不同的信息,甚至在发送电子邮件时执行完全不同的操作时,您可以更改LogEvent方法或事件处理程序,而无需接触Email类。 此外,其他类也可以订阅事件,而无需更改Email类,并且在发送电子邮件时可以发生大量事情。

现在维护你的代码要容易得多,其他人更有可能重用你的代码,因为他们知道他们不必为了改变处理方式而挖掘 20 个不同类的内脏。

大编辑:更多关于代表的信息。 如果你通读这里:好奇心是幸福:C# Events vs Delegate(我保证,我会保持最少的链接),你会看到作者是如何进入事件基本上是特殊类型的委托的事实。 他们期望某个方法签名(即 (object sender, EventArgs e) ),并且可以添加多个方法(+=),以便在引发该方法时执行。 还有其他差异,但这些是您会注意到的主要差异。 那么代表有什么用呢?

假设您想为Email类的客户端提供一些有关如何发送邮件的选项。 您可以为此定义一系列方法:

Public Class Email
{
   public string To {get;set;}
   //Omitted code
   public void Send(MailMethod method)
   {
     switch(method)
     {
       case MailMethod.Imap:
         ViaImap();
         break;
       case MailMethod.Pop:
         ViaPop();
         break;
      }
   }
   private void ViaImap() {...}
   private void ViaPop() {...}
}

这很好用,但是如果您以后想添加更多选项,则必须编辑您的类(以及此处假设的MailMethod枚举)。 如果你声明了一个委托,你可以把这种决定推迟到客户端,并使你的类更加灵活:

Public Class Email
{
   public Email()
   {
     Method = ViaPop;//declare the default method on instantiation
   }
   //define the delegate
   public delegate void SendMailMethod(string title, string message);
   //declare a variable of type SendMailMethod
   public SendMailMethod Method;
   public string To {get;set;}
   //Omitted code
   public void Send()
   {
     //assume title and message strings have been determined already
     Method(title, message);
   }
   public void SetToPop()
   {
     this.Method = ViaPop;
   }
   public void SetToImap()
   {
     this.Method = ViaImap;
   }
   //You can write some default methods that you forsee being needed
   private void ViaImap(string title, string message) {...}
   private void ViaPop(string title, string message) {...}
}

现在,客户端可以将您的类与自己的方法一起使用,或者提供自己的方法来发送邮件,几乎可以随心所欲地发送邮件:

var regularEmail = new Email();
regularEmail.SetToImap();
regularEmail.Send();
var reallySlowEmail = new Email();
reallySlowEmail.Method = ViaSnailMail;
public void ViaSnailMail(string title, string message) {...}

现在,您的类耦合得不那么紧密,并且更容易维护(并为其编写测试! 当然还有其他使用委托的方法,lambda 有点把事情提升了一个档次,但这应该足以进行简单的介绍。

好吧,我知道这个答案严格来说不正确,但我会告诉你我是如何理解它们的。

所有函数都有一个内存地址,有些函数是简单的数据获取/设置。将所有变量视为只有两种方法 - get 和 set 的函数会有所帮助。您非常习惯通过引用传递变量,这意味着(简单地)您将指针传递给它们的内存,这使其他一些代码能够通过使用"="和"=="隐式调用它们的get/set方法。

现在将该概念转换为函数和代码。某些代码和函数具有您为其指定的名称(如变量名称)。您习惯于通过调用它们的名称来执行这些函数;但该名称只是其内存位置的同义词(简单地说)。通过调用该函数,您将使用其名称取消引用其内存地址,然后调用位于该内存地址的方法。

正如我所说,这一切都非常简单,并且在各种方面都是不正确的。但它对我有帮助。

那么 - 是否可以传递函数的内存地址但不调用它?以同样的方式,您将对变量的引用传递给它而不计算它?即什么相当于调用

DoSomeFunction(ref variablePointer) 

好吧,对函数的引用称为委托。但是因为函数也可以接受参数(变量不能),所以你需要使用比 ref 更详细的调用语法。您将要进行的呼叫设置为委托结构,并将该委托结构传递给收件人,收件人可以立即评估(呼叫)该代理,或将其存储以供以后使用。

"供以后使用的存储"是理解事件处理程序的关键。围绕事件处理程序的特殊(并且有些令人困惑)语法只是设置函数指针(委托)并将其添加到函数指针列表中的另一种方法,收件人类可以在某个方便的时间评估该列表。

查看事件处理程序的一种简单方法是;

class myClass
{ 
     public List<delegate> eventHandlers = new  List<delegate>();
     public void someMethod()
     {
          //... do some work
          //... then call the events
          foreach(delegate d in eventHandlers)
          {
               // we have no idea what the method name is that the delegate
               // points to, but we dont need to know - the pointer to the 
               // function is stored as a delegate, so we just execute the 
               // delegate, which is a synonym for the function.
               d();
          }
      }
 }
 public class Program()
 {
      public static void Main()
      {
          myClass class1 = new myClass();
          // 'longhand' version of setting up a delegate callback
          class1.eventHandlers.Add(new delegate(eventHandlerFunction));
          // This call will cause the eventHandlerFunction below to be 
          // called
          class1.someMethod();
          // 'shorthand' way of setting up a delegate callback
          class1.eventHandlers.Add(() => eventHandlerFunction());
      }
      public static eventHandlerFunction()
      {
           Console.WriteLine("I have been called");
      }

当您希望委托的调用者将一些值传递给函数时,它会稍微复杂一些,但除此之外,所有委托概念都与"ref"变量的概念相同 - 它们是对稍后将执行的代码的引用,通常您将它们作为回调传递给其他类, 谁将决定何时以及是否执行它们。在厄勒语言中,代表几乎与"函数指针"或(在心爱的早已离开的楠塔基特快船)"代码块"相同。它比简单地传递代码块的内存地址要复杂得多,但如果你坚持这个概念,你就不会出错。

希望有帮助。

考虑委托使用的最简单方法是考虑何时要调用方法,但您还不知道是哪一个(或多个)。

以控件的Click事件处理程序为例。它使用EventHandler委托。签名void EventHandler(object sender, EventArgs e); 。这个委托的目的是,当有人单击控件时,我希望能够调用零个或多个具有EventHandler签名的方法,但我还不知道它们是什么。此委托允许我有效地调用未知的未来方法。

另一个例子是 LINQ 的.Select(...)运算符。它具有签名IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)。此方法包含一个委托Func<TSource, TResult> selector。此方法的作用是从source中获取一系列值,并应用尚不为人知的投影来生成一系列TResult

最后,另一个很好的例子是 Lazy<T> .此对象具有具有以下签名的构造函数:public Lazy(Func<T> valueFactory)Lazy<T>的工作是将T的实例化延迟到第一次使用,但随后保留该值以供将来使用。它可能是一个昂贵的实例化,如果我们不需要该对象,那么避免它是理想的,但是如果我们需要它不止一个,我们不希望受到成本的影响。 Lazy<T>处理所有线程锁定等,以确保只创建T的一个实例。但是Func<T> valueFactory返回的T值可以是任何东西 - Lazy<T>的创建者不知道委托会是什么,他们也不应该知道。

对我来说,这是了解代表的最重要的事情。

为什么要使用事件和委托而不是调用方法?

在您发布的示例的上下文中,如果要异步发送电子邮件,则必须实现通知机制。

有关示例,请参阅 SmtpClient 实现:https://msdn.microsoft.com/en-us/library/system.net.mail.smtpclient.sendcompleted%28v=vs.110%29.aspx

如果需要更多的解释,而不是代码示例,我将尝试解释为什么你会在上面给出的示例中使用委托或事件。

想象一下,你想知道电子邮件是否在你调用Email.Send()后被发送。在电子邮件类中,您将有两个事件 - 一个用于发送失败,另一个用于成功发送。当电子邮件类发送时没有错误,它将查看是否有任何订阅者访问"SuccessSend()"事件,如果有,它会引发该事件。然后,这将通知希望接收发送成功时的订阅者,以便他们可以执行其他任务。

因此,您可以有一个成功发送通知的事件处理程序,并且在此处理程序中,您可以调用另一个方法 (DoMoreWork())。如果 Email.Send() 失败,您可能会收到此通知,并调用另一个记录失败以供以后参考的方法。

关于委托,如果有三个不同的电子邮件类使用不同的功能(或服务器)来发送邮件,则调用 Email.Send() 方法的客户端可以提供发送电子邮件时使用的相关电子邮件类。电子邮件类将使用IEmail接口,三个电子邮件类将实现IEmail(收件人,发件人,主题,正文,附件,HTMLBody等),但可以以不同的方式执行交互/规则。

一个

可能需要主题,另一个需要附件,一个可以使用 CDONTS,另一个使用不同的协议。客户端可以根据安装位置确定是否需要使用 CDONTS,或者它可能位于需要附件的应用区域中,或者将正文格式化为 HTML。这样做是为了消除客户端和应检查这些检查和逻辑的所有位置的逻辑负担,并将其移动到相关类的单个版本中。然后,客户端只需在提供要在其构造函数中使用的正确对象(或使用可设置属性)后调用 Email.Send()。如果需要修复或更改特定电子邮件对象的代码 - 它将在一个地方执行,而不是在客户端中找到所有区域并在那里更新。想象一下,如果您的电子邮件类被几个不同的应用程序使用......