12

我目前很难理解和使用 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#。

非常感谢!

4

9 回答 9

17

您将创建四个事件和引发它们的方法,以及一个新的基于 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{}部分只是避免必须进行空检查的技巧。它为每个事件订阅一个无操作事件处理程序
  • 事件声明是类似字段的事件。实际创建的是变量事件。在类中,您会看到变量;在课堂之外,您会看到该事件。

有关活动的更多详细信息,请参阅我的活动/代表文章。

于 2008-10-08T05:29:53.963 回答
5

您必须为此定义一个委托

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.
}
于 2008-10-08T05:20:29.817 回答
4

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

您还可以在此处阅读 C# 与 Java 事件的简短比较。

于 2008-10-08T05:22:32.043 回答
2

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

  1. 创建一个将包含事件参数的类(派生自 EventArgs)。
公共类 ComputerEventArgs : EventArgs
{
  电脑电脑;
  // 构造函数、属性等
}
  1. 在要触发事件的类上创建一个公共事件。
    class ComputerEventGenerator // 我选了一个可怕的名字顺便说一句。
    {
      公共事件 EventHandler<ComputerEventArgs> ComputerStarted;
      公共事件 EventHandler<ComputerEventArgs> ComputerStopped;
      公共事件 EventHandler<ComputerEventArgs> ComputerReset;
    ...
    }
  1. 调用事件。
    类 ComputerEventGenerator
    {
    ...
      私人无效 OnComputerStarted(计算机)
      {
        EventHandler<ComputerEventArgs> temp = ComputerStarted;
        if (temp != null) temp(this, new ComputerEventArgs(computer)); // 如果事件是静态的,则用 null 替换 "this"
      }
     }
  1. 为事件附加一个处理程序。
    无效的加载()
    {
      ComputerEventGenerator computerEventGenerator = new ComputerEventGenerator();
      computerEventGenerator.ComputerStarted += new EventHandler<ComputerEventArgs>(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”。我讨厌电脑。

于 2008-10-08T05:32:34.770 回答
1

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

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 个委托减少为一个。在这种情况下,这将是一个更清洁的解决方案,尤其是。带有计算机状态的枚举,以防以后扩展。但基本技术保持不变:

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

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

于 2008-10-08T05:23:59.823 回答
1

在 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 方法。

于 2008-10-08T05:24:52.990 回答
0

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

给定以下用作事件的委托:

// 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();
    }
}

尽管参数可以是任何东西,但 object sender 和 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;
}
于 2008-10-08T05:47:34.630 回答
0

非常感谢大家的回答!最后我开始明白发生了什么。就一件事; 似乎如果每个事件都有不同数量/类型的参数,我需要创建一个不同的 :: 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 类)

干杯!

于 2008-10-08T14:33:40.043 回答
0

好的,最后的澄清!:所以这几乎是我可以在代码方面做的最好的事情来实现这些事件?

   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));
        }
    }
}
于 2008-10-08T16:36:56.533 回答