【问题标题】:C# event handling (compared to Java)C# 事件处理(与 Java 相比)
【发布时间】:2010-09-15 22:28:06
【问题描述】:

我目前很难理解和使用 delegates 在 C# 中实现事件。我习惯了Java的做事方式:

  1. 为包含许多方法定义的侦听器类型定义一个接口
  2. 如果我对侦听器中定义的所有事件不感兴趣,请为该接口定义适配器类以使事情变得更容易
  3. 在引发事件的类中定义 Add、Remove 和 Get[] 方法
  4. 定义受保护的触发方法来完成循环添加的侦听器列表并调用正确方法的繁琐工作

我理解(并且喜欢!) - 我知道我可以在 c# 中完全一样地做到这一点,但似乎为 c# 准备了一个新的(更好的?)系统。在阅读了无数解释 c# 中委托和事件使用的教程之后,我仍然没有更接近真正理解发生了什么:S


简而言之,对于以下方法,我将如何在 c# 中实现事件系统:

void computerStarted(Computer computer);
void computerStopped(Computer computer);
void computerReset(Computer computer);
void computerError(Computer computer, Exception error);

^ 以上方法取自我曾经制作的一个 Java 应用程序,我正在尝试将其移植到 c#。

非常感谢!

【问题讨论】:

    标签: c# event-handling delegates


    【解决方案1】:

    您将创建四个事件和引发它们的方法,以及一个新的基于 EventArgs 的类来指示错误:

    public class ExceptionEventArgs : EventArgs
    {
        private readonly Exception error;
    
        public ExceptionEventArgs(Exception error)
        {
             this.error = error;
        }
    
        public Error
        {
             get { return error; }
        }
    }
    
    public class Computer
    {
        public event EventHandler Started = delegate{};
        public event EventHandler Stopped = delegate{};
        public event EventHandler Reset = delegate{};
        public event EventHandler<ExceptionEventArgs> Error = delegate{};
    
        protected void OnStarted()
        {
            Started(this, EventArgs.Empty);
        }
    
        protected void OnStopped()
        {
            Stopped(this, EventArgs.Empty);
        }
    
        protected void OnReset()
        {
            Reset(this, EventArgs.Empty);
        }
    
        protected void OnError(Exception e)
        {
            Error(this, new ExceptionEventArgs(e));
        }
    }
    

    然后类将使用方法或匿名函数订阅事件:

    someComputer.Started += StartEventHandler; // A method
    someComputer.Stopped += delegate(object o, EventArgs e)
    { 
        Console.WriteLine("{0} has started", o);
    };
    someComputer.Reset += (o, e) => Console.WriteLine("{0} has been reset");
    

    以上几点需要注意:

    • OnXXX 方法受到保护,因此派生类可以引发事件。这并不总是必要的 - 做你认为合适的事情。
    • 每个事件声明中的delegate{} 部分只是避免进行空值检查的一个技巧。它为每个事件订阅了一个无操作事件处理程序
    • 事件声明是类字段事件。实际创建的是一个变量一个事件。在类中,您会看到变量;在课堂之外,您会看到活动。

    有关事件的更多详细信息,请参阅我的 events/delegates 文章。

    【讨论】:

    • 一切看起来都不错,但我不喜欢在事件中添加空代表的模式。我知道这意味着您不必检查 null,但这意味着您为每个类分配浪费的内存,并且即使没有侦听器,您也总是调用回调。检查 null 并不难... +1 虽然
    • 这是一个非常轻微的性能损失(非常小)与可读性增益的权衡。我总是首先以可读性为目标,然后在必要时进行微优化。
    • 过早的优化不是我的意图,但在我看来,这与在可读性/可用性方面几乎没有收益的无偿低效之间存在差异(事实上我认为它的可读性较差,因为我希望在正确的事件引发代码中看到 null 检查!)。
    • 我想说你只希望看到空检查,因为你不习惯这种模式——一旦你知道它在那里以及为什么,我相信它会让它更具可读性。这当然是我的经验。在我看来,可读性的好处值得微小的额外内存占用(续)
    • 除非您知道您将创建 很多 个实例。在这种情况下,我可能会创建一个“常量”无操作委托——在单个实例之外没有内存浪费,并且只有微不足道的无操作执行成本。请注意,在多线程类中,无效性检查需要更复杂(续)
    【解决方案2】:

    您必须为此定义一个委托

    public delegate void ComputerEvent(object sender, ComputerEventArgs e);
    

    ComputerEventArgs 的定义如下:

    public class ComputerEventArgs : EventArgs
    {
        // TODO wrap in properties
        public Computer computer;
        public Exception error;
    
        public ComputerEventArgs(Computer aComputer, Exception anError)
        {
            computer = aComputer;
            error = anError;
        }
    
        public ComputerEventArgs(Computer aComputer) : this(aComputer, null)
        {
        }
    }
    

    触发事件的类有这些:

    public YourClass
    {
        ...
        public event ComputerEvent ComputerStarted;
        public event ComputerEvent ComputerStopped;
        public event ComputerEvent ComputerReset;
        public event ComputerEvent ComputerError;
        ...
    }
    

    这是您为事件分配处理程序的方式:

    YourClass obj = new YourClass();
    obj.ComputerStarted += new ComputerEvent(your_computer_started_handler);
    

    您的处理程序是:

    private void ComputerStartedEventHandler(object sender, ComputerEventArgs e)
    {
       // do your thing.
    }
    

    【讨论】:

    • 如果您在之前/之后的状态中使用枚举,那么您只需要 1 个事件,ComputerStateChanged
    • 我更喜欢为每个事件设置单独的事件。我已经看到使用 Enums 表示计算机状态的代码 - 但发生的情况是处理程序将有一个 switch(e.State),然后无论如何都会为每个状态调用不同的方法。
    • 您不必定义自己的委托 - 这就是 EventHandler 的用途。事件名称也非常规 - 事件 raising 方法通常以“On”开头,但事件本身不是。例如,Control.Click 事件由 Control.OnClick() 引发。
    • @Jon - 很酷。我仍在使用 C# 2.0,所以我还没有尝试过。
    • EventHandler 存在于 C# 2.0
    【解决方案3】:

    主要区别在于 C# 中的事件不是基于接口的。相反,事件发布者声明了委托,您可以将其视为函数指针(尽管不完全相同:-))。然后订阅者将事件原型实现为常规方法,并将委托的新实例添加到发布者的事件处理程序链中。阅读有关delegatesevents 的更多信息。

    您还可以阅读 C# 与 Java 事件的简短比较here

    【讨论】:

    • 很好的答案,而不是仅仅给出代码你给了他一个解释。
    • 给一个人一条鱼,他可以吃一天。教一个人钓鱼,他可以吃一辈子。 :-)
    【解决方案4】:

    首先,.Net 中有一个标准方法签名,通常用于事件。这些语言完全允许将任何类型的方法签名用于事件,并且有一些专家认为该约定存在缺陷(我大多同意),但事实就是如此,我将在此示例中遵循它。

    1. 创建一个包含事件参数的类(派生自 EventArgs)。
    公共类 ComputerEventArgs : EventArgs { 电脑电脑; // 构造函数、属性等 }
    1. 在要触发事件的类上创建一个公共事件。
    class ComputerEventGenerator // 我选了一个可怕的名字顺便说一句。 { 公共事件 EventHandler ComputerStarted; 公共事件 EventHandler ComputerStopped; 公共事件 EventHandler ComputerReset; ... }
    1. 调用事件。
    类 ComputerEventGenerator { ... 私人无效 OnComputerStarted(计算机) { EventHandler temp = ComputerStarted; if (temp != null) temp(this, new ComputerEventArgs(computer)); // 如果事件是静态的,则用 null 替换 "this" } }
    1. 为事件附加一个处理程序。
    无效的加载() { ComputerEventGenerator computerEventGenerator = new ComputerEventGenerator(); computerEventGenerator.ComputerStarted += new EventHandler(ComputerEventGenerator_ComputerStarted); }
    1. 创建刚刚附加的处理程序(主要通过在 VS 中按 Tab 键)。
    私人无效ComputerEventGenerator_ComputerStarted(对象发送者,ComputerEventArgs args) { if (args.Computer.Name == "HAL9000") ShutItDownNow(args.Computer); }
    1. 完成后不要忘记分离处理程序。 (忘记这样做是 C# 中内存泄漏的最大来源!)
    无效 OnClose() { ComputerEventGenerator.ComputerStarted -= ComputerEventGenerator_ComputerStarted; }

    就是这样!

    编辑:老实说,我无法弄清楚为什么我的编号点都显示为“1”。我讨厌电脑。

    【讨论】:

    • 我不知道编辑为什么给我这么多麻烦。起初,代码块除了缩进什么都不会做,所以我不得不把它改成 pre 标签。然后我仔细编号的序列现在全是 1。这件事让我看起来很可笑!
    • Jeffrey:这是因为编号的列表项需要按顺序排列。您正在交替使用两种不同类型的块的代码和列表。
    • 但是我没有将列表编码为列表,我只是自己输入了数字。它可以看到我输入“3”的位置。数字“哦,这是一个编号的项目。我将其更改为 1。”电脑!
    【解决方案5】:

    有几种方法可以做你想做的事。 最直接的方式是为托管类中的每个事件定义委托,例如

    public delegate void ComputerStartedDelegate(Computer computer);
    protected event ComputerStartedDelegate ComputerStarted;
    public void OnComputerStarted(Computer computer)
    {
        if (ComputerStarted != null)
        {
            ComputerStarted.Invoke(computer);
        }
    }
    protected void someMethod()
    {
        //...
        computer.Started = true;  //or whatever
        OnComputerStarted(computer);
        //...
    }
    

    任何对象都可以通过以下方式“监听”此事件:

    Computer comp = new Computer();
    comp.ComputerStarted += new ComputerStartedDelegate(
        this.ComputerStartedHandler);
    
    protected void ComputerStartedHandler(Computer computer)
    {
        //do something
    }
    

    执行此操作的“推荐标准方法”是定义 EventArgs 的子类来保存计算机(以及旧/新状态和异常)值,将 4 个委托减少为一个。在这种情况下,这将是一个更清洁的解决方案,尤其是。带有计算机状态的枚举,以防以后扩展。但基本技术保持不变:

    • 委托定义事件处理程序/侦听器的签名/接口
    • 事件数据成员是“侦听器”列表

    使用 -= 语法而不是 += 删除侦听器

    【讨论】:

      【解决方案6】:

      在 c# 中,事件是委托。它们的行为方式类似于 C/C++ 中的函数指针,但它们是派生自 System.Delegate 的实际类。

      在这种情况下,创建一个自定义 EventArgs 类来传递 Computer 对象。

      public class ComputerEventArgs : EventArgs
      {
        private Computer _computer;
      
        public ComputerEventArgs(Computer computer) {
          _computer = computer;
        }
      
        public Computer Computer { get { return _computer; } }
      }
      

      然后从生产者那里暴露事件:

      public class ComputerEventProducer
      {
        public event EventHandler<ComputerEventArgs> Started;
        public event EventHandler<ComputerEventArgs> Stopped;
        public event EventHandler<ComputerEventArgs> Reset;
        public event EventHandler<ComputerEventArgs> Error;
      
        /*
        // Invokes the Started event */
        private void OnStarted(Computer computer) {
          if( Started != null ) {
            Started(this, new ComputerEventArgs(computer));
          }
        }
      
        // Add OnStopped, OnReset and OnError
      
      }
      

      事件的消费者然后将处理函数绑定到消费者上的每个事件。

      public class ComputerEventConsumer
      {
        public void ComputerEventConsumer(ComputerEventProducer producer) {
          producer.Started += new EventHandler<ComputerEventArgs>(ComputerStarted);
          // Add other event handlers
        }
      
        private void ComputerStarted(object sender, ComputerEventArgs e) {
        }
      }
      

      当 ComputerEventProducer 调用 OnStarted 时,将调用 Started 事件,该事件又将调用 ComputerEventConsumer.ComputerStarted 方法。

      【讨论】:

      • 事件是不是代表。事件是你订阅的东西。
      【解决方案7】:

      委托声明了一个函数签名,当它用作类上的事件时,它还充当登记调用目标的集合。事件的 += 和 -= 语法用于将目标添加到列表中。

      鉴于以下委托用作事件:

      // arguments for events
      public class ComputerEventArgs : EventArgs
      {
          public Computer Computer { get; set; }
      }
      
      public class ComputerErrorEventArgs : ComputerEventArgs
      {
          public Exception Error  { get; set; }
      }
      
      // delegates for events
      public delegate void ComputerEventHandler(object sender, ComputerEventArgs e);
      
      public delegate void ComputerErrorEventHandler(object sender, ComputerErrorEventArgs e);
      
      // component that raises events
      public class Thing
      {
          public event ComputerEventHandler Started;
          public event ComputerEventHandler Stopped;
          public event ComputerEventHandler Reset;
          public event ComputerErrorEventHandler Error;
      }
      

      您可以通过以下方式订阅这些事件:

      class Program
      {
          static void Main(string[] args)
          {
              var thing = new Thing();
              thing.Started += thing_Started;
          }
      
          static void thing_Started(object sender, ComputerEventArgs e)
          {
              throw new NotImplementedException();
          }
      }
      

      虽然参数可以是任何东西,但对象发送者和 EventArgs e 是一个使用非常一致的约定。 += thing_started 将首先创建一个指向目标方法的委托实例,然后将其添加到事件中。

      在组件本身上,您通常会添加触发事件的方法:

      public class Thing
      {
          public event ComputerEventHandler Started;
      
          public void OnStarted(Computer computer)
          {
              if (Started != null)
                  Started(this, new ComputerEventArgs {Computer = computer});
          }
      }
      

      如果没有代表添加到事件中,您必须测试 null。但是,当您进行方法调用时,将调用所有已添加的委托。这就是事件返回类型为 void 的原因 - 没有单一的返回值 - 因此,要反馈信息,您将拥有 EventArgs 上的属性,事件处理程序将更改这些属性。

      另一个改进是使用通用的 EventHandler 委托,而不是为每种类型的 args 声明一个具体的委托。

      public class Thing
      {
          public event EventHandler<ComputerEventArgs> Started;
          public event EventHandler<ComputerEventArgs> Stopped;
          public event EventHandler<ComputerEventArgs> Reset;
          public event EventHandler<ComputerErrorEventArgs> Error;
      }
      

      【讨论】:

        【解决方案8】:

        非常感谢大家的回答!最后我开始明白发生了什么。就一件事;似乎如果每个事件都有不同数量/类型的参数,我需要创建一个不同的 :: EventArgs 类来处理它:

        public void computerStarted(Computer computer);
        public void computerStopped(Computer computer);
        public void computerReset(Computer computer);
        public void breakPointHit(Computer computer, int breakpoint);
        public void computerError(Computer computer, Exception exception);
        

        这需要三个类来处理事件!? (两个自定义,一个使用默认的 EventArgs.Empty 类)

        干杯!

        【讨论】:

        • 正如我在回答中所说,一些专家认为 .Net 对事件的转换是有缺陷的。您刚刚强调了主要反对意见之一。 (另一个是“对象发送者”约定,这是不必要的,并且在许多情况下会破坏封装。) EventArgs 通常在实践中可以正常工作。
        • 顺便说一句,我希望您在选择获胜者之前阅读每个答案的每一个字! ;-P
        • 哈哈,我给每个人都投了赞成票——所有的答案都很好。至于谁是“赢家”……啊!
        【解决方案9】:

        好的,最后的澄清!:所以这几乎是我能在代码方面实现这些事件的最好方法了吗?

           public class Computer {
        
                public event EventHandler Started;
        
                public event EventHandler Stopped;
        
                public event EventHandler Reset;
        
                public event EventHandler<BreakPointEvent> BreakPointHit;
        
                public event EventHandler<ExceptionEvent> Error;
        
                public Computer() {
                    Started = delegate { };
                    Stopped = delegate { };
                    Reset = delegate { };
                    BreakPointHit = delegate { };
                    Error = delegate { };
                }
        
                protected void OnStarted() {
                    Started(this, EventArgs.Empty);
                }
        
                protected void OnStopped() {
                    Stopped(this, EventArgs.Empty);
                }
        
                protected void OnReset() {
                    Reset(this, EventArgs.Empty);
                }
        
                protected void OnBreakPointHit(int breakPoint) {
                    BreakPointHit(this, new BreakPointEvent(breakPoint));
                }
        
                protected void OnError(System.Exception exception) {
                    Error(this, new ExceptionEvent(exception));
                }
            }
        }
        

        【讨论】:

          猜你喜欢
          • 2018-03-27
          • 1970-01-01
          • 2013-01-17
          • 2020-12-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多