事件和代表与调用方法

时间:2015-03-07 11:43:10

标签: c#

我希望这个问题与其他人没有密切关系,但其他人似乎并没有填补知识空白。

这似乎是尝试和理解事件和代表的热门话题,在阅读了许多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()方法是否可以订阅电子邮件?这会有用吗?任何帮助我真正理解事件和代表的人都将不胜感激。

此致

5 个答案:

答案 0 :(得分:6)

我在实际编程中发现使用事件和代理的最大原因是轻松完成代码维护鼓励代码重用的任务。

当一个类调用另一个类中的方法时,这些类是“紧耦合”的。你紧密耦合的课程越多,改变其中一个课程就越困难,而不必改变其他课程。你可以在那时写一个大班。

使用事件会使事情更“松散耦合”,并且更容易更改一个类而不必打扰其他人。

以上面的例子为例,假设我们有第三个类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紧密相连。如果我们更改LogEventLogger方法的签名,我们还必须对Email进行更改。当你处理一个中型项目时,你是否看到这会如何迅速成为一场噩梦?此外,没有人甚至想尝试使用LogEvent方法,因为他们知道如果他们需要对它进行任何改变,他们将不得不开始改变其他类,那应该是一个下午的价值工作很快变成一周。所以相反,他们编写了一个新方法,或者一个新类,然后紧密耦合到他们正在做的任何其他事情,事情变得臃肿,每个程序员开始进入他们自己的代码的“贫民窟”。当你必须稍后进入并弄清楚该程序到底在做什么或追捕一个bug时,这非常非常糟糕。

如果您在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个不同类的内容只是为了改变一些东西是什么处理。

大编辑:有关代表的更多信息。如果您在这里阅读:Curiosity is Bliss: C# Events vs Delegates(我会保持最低限度的链接,我保证),您会看到作者如何了解事件基本上是特殊类型的委托。他们期望某个方法签名(即(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) {...}

现在你的类的耦合程度更低,更易于维护(并为其编写测试!)。当然还有其他方法可以使用代表,而lambdas则可以解决这个问题,但这应该足以让我们进行简单的介绍。

答案 1 :(得分:1)

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

所有函数都有一个内存地址,有些函数可以简单地获取/设置数据。它有助于将所有变量视为只有两种方法的函数 - 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”变量的概念相同 - 它们是对代码的引用,它将是稍后执行,通常将它们作为回调传递给其他类,后者将决定何时以及是否执行它们。在earler语言中,委托与“函数指针”或(在心爱的长期离开的Nantucket Clipper)“代码块”几乎相同。它比简单地传递一段代码的内存地址要复杂得多,但是如果你坚持这个概念,你就不会出错。

希望有所帮助。

答案 2 :(得分:1)

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

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

另一个例子是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实例。但T返回的Func<T> valueFactory的值可以是任何内容 - Lazy<T>的创建者不知道代表将是什么,也不应该。< / p>

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

答案 3 :(得分:0)

  

为什么要使用事件和代理来调用方法?

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

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

答案 4 :(得分:0)

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

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

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

关于代表,如果有三个不同的电子邮件类使用不同的功能(或服务器)来发送邮件,则调用Email.Send()方法的客户端可以提供在发送电子邮件时使用的相关电子邮件类。 电子邮件类将使用IEmail接口,三个电子邮件类将实现IEmail(To,From,Subject,Body,Attachments,HTMLBody等),但可以以不同方式执行交互/规则。

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