事件驱动编程

4
我一直在阅读这篇MSDN文章这个问题,试图理解.NET中的事件。不幸的是,我还没有完全明白,遇到了很多困难。我正在尝试将此技术整合到我的项目中,但成功率很低。
基本上,我有一个会读取数字的类。每当它遇到一个新数字时,我希望它触发一个名为numberChanged的事件。
因此,我设置了我的事件public event EventHandler numberChanged;。稍后,当它遇到一个与先前数字不同的数字时,我会触发我的事件。
if(currentNumber != previousNumber){
     if(numberChanged != null){
          numberChanged(this, new EventArgs());
     }
}

但我现在遇到了一个问题,就是无法“订阅”此事件。如果我执行 numberChanged += [something to do here] ,它会报错,说numberChanged是一个事件而不是一个类型。

请问我的解释是否足够清晰,以便能够得到一些帮助?非常感谢。


你应该像这样订阅事件:yourclassInstance.numberChanged+=()=>{}; 或者 numberChanged+=functionSignature; - Monah
2
你能否发布导致错误的确切代码?你的 [something to do here] 代码很可能是引起问题的原因。 - BJ Myers
@BJMyers 这只是一个方法名称,我宁愿不展示。 - JShell
1
创建一个最小化、完整化和可验证化的示例将使得其他人更容易帮助您进行调试。 - BJ Myers
@JesseShellabarger 检查一下我的答案,如果需要更多帮助,请继续。我认为最好定义自己的委托来触发具有2个参数(旧值和新值)的numberchanged,这样它将更具可重用性。 - Monah
显示剩余3条评论
3个回答

6

有许多方法可以处理它,最基本的是创建一个函数:

public void MyNumberChangedHandler(object sender, EventArgs e)
{
    //Your code goes here that gets called when the number changes
}

通常在构造函数中,你只需要订阅一次(one time only):

代码如下:

numberChanged += MyNumberChangedHandler;

或者,您可以使用称为匿名(lambda)方法的东西,该方法也会在构造函数中分配(通常是):
numberChanged += (sender, e) => {
    //Your code here to handle the number changed event
};

为了详细说明一下,使用 lambda 方法时必须小心,因为你可能会创建内存泄漏和僵尸对象。.NET 内存垃圾收集器是一个标记-清除系统,当对象不再使用时会将其移除。这篇文章展示了删除 lambda 事件处理程序有多么困难:如何删除 lambda 事件处理程序
即使已经被释放,拥有活动事件处理程序的对象也可能仍然存在。以下是创建僵尸对象的示例(在 Fiddle 中无法运行,但可以复制到您自己的控制台应用程序中):https://dotnetfiddle.net/EfNpZ5 输出内容如下:
I'm still alive
I'm still alive
I was disposed!
Press any key to quit
I'm still alive
I'm still alive
I'm still alive.

1
在程序化添加事件处理程序的话题上,像这样的语句:myButton.Click += new EventHandler(myButton_Click);myButton.Click += myButton_Click 有什么区别? - sab669
1
@sab669 没有技术上的问题。 - Ron Beyer
2
@sab669 这只是C# 2.0引入的一种语法糖。 - Setsu
IDisposable.Dispose 方法用于释放非托管资源,与释放托管内存无关。IDisposable.Dispose 和垃圾回收器之间的关系是,如果您有一个 Dispose 方法来释放非托管资源,则需要创建一个终结器以确保即使未调用 Dispose,非托管资源也会被释放。这与泄漏事件处理程序无关。 - Martin Liversage
@MartinLiversage 抱歉,我不确定我完全理解了。在我的上一份工作中,我养成了一个AttachEvents()方法的习惯,它会使用new EventHandler()语法附加事件,然后我们会有一个DettachEvents()方法,我们会从窗体设计器的Dispose()方法中调用它。我从来没有学到为什么要这样做。我认为这基本上是GC的工作。这或多或少是你所指的吗? - sab669
@MartinLiversage 我了解这点,但是您可以使用Dispose方法来在析构时执行其他任务,而不仅仅是释放非托管资源。例如,将对象放入using块中会导致调用Dispose方法,在其中您可以清理该对象。我想强调的是,如果您不摆脱事件处理程序,GC可以使对象保持活动状态。 - Ron Beyer

4
作为C#编程世界中的所有其他内容,事件概念也遵循特定的规则并具有自己的语法。措辞如下:
  • 定义为EventHandler的事件实际上只是一种特殊方法(委托)签名的快捷方式-public delegate void EventHandler(object sender, EventArgs e)[1]。在C#中,每当您有一个签名时,您总是知道需要在右侧或作为参数中编写什么,以连接/调用某些对象/方法等。
  • 事件定义后,需要订阅以便在发生事件时获得通知。订阅事件的语法为+=。自然而然地,取消订阅事件的语法为-=。MSDN表示语法应为object.event += eventHandler(或object.event += new EventHandler(eventHandler);
  • 因此,在事件被定义之后(event Event SomeEvent;),所有剩下的就是创建一个可以绑定到此事件的方法。该方法必须具有与EventHandler相同的签名,因此它应该与[1]的签名匹配,并且可以是private void numberChangedEventHandler(object sender, EventArgs eventArguments)

现在您知道需要在+=右侧编写什么内容了。

例如:

public class NumberSequence
{
    // numbers to be compared
    private readonly List<int> numbers = new List<int>();
    // used to generate a random collection
    private readonly Random random = new Random();
    // tell me if the previous and next number are different
    public event EventHandler DifferentNumbersEvent;

    public NumberSequence()
    {
        // fill the list with random numbers
        Enumerable.Range(1, 100).ToList().ForEach(number =>
        {
            numbers.Add(random.Next(1, 100));
        });
    }

    public List<int> Numbers { get { return numbers; } }

    public void TraverseList()
    {
        for (var i = 1; i < this.numbers.Count; i++)
        {
            if (this.numbers[i - 1] != this.numbers[i])
            {
                if (this.DifferentNumbersEvent != null)
                {
                    // whoever listens - inform him
                    this.DifferentNumbersEvent(this, EventArgs.Empty);
                }
            }
        }
    }
}

在使用类之前,定义事件处理程序,它将会“监听”并在事件被“触发”时被调用(再次强调措辞):

private void differentNumberEventHandler(Object sender, EventArgs eventArguments)
{
    Console.WriteLine("Different numbers...");
}

而且用法:

var ns = new NumberSequence();
ns.DifferentNumbersEvent += differentNumberEventHandler;
ns.TraverseList();

除了这种符号表示法(lambda / 匿名方法 / ...)外,其他都只是语法糖,例如:

object.Event += (s, e) => { // code ... };object.Event += (Object sender, EventArgs eventArguments) => { // code ... }; 是相同的。你认识这个签名吗?它与 private void differentNumberEventHandler... 相同。

通常我们需要通过事件传递信息,在这种情况下,也许我们想要看到这两个数字。C# 允许您使用自定义事件参数轻松地实现此目的。只需创建一个继承 EventArgs 类的类,并添加应该传递的数据的属性,即这种情况下的数字:

public class NumbersInfoEventArgs : EventArgs
{
    public int Number1 { get; set; }
    public int Number2 { get; set; }
}

然后在声明事件时指定它将传递类型为NumbersInfoEventArgs的数据(再次签名):

public event EventHandler<NumbersInfoEventArgs> DifferentNumbersEvent;
...
this.DifferentNumbersEvent(this, new NumbersInfoEventArgs
{
    Number1 = this.numbers[i - 1],
    Number2 = this.numbers[i]
});

最后但并非不重要的是,事件处理函数的签名应该与事件的签名匹配:
private void differentNumberEventHandler(Object sender, NumbersInfoEventArgs eventArguments)
{
    Console.WriteLine("Different numbers {0} - {1}", eventArguments.Number1, eventArguments.Number2);
}

And voila, the output is:

Different numbers 89 - 86
Different numbers 86 - 53
Different numbers 53 - 12
Different numbers 12 - 69

1
就最佳实践而言,任何继承EventArgs类的类都应该包含单词“EventArgs”,例如,NumbersInfo应该被称为NumbersInfoEventArgs。请参见https://msdn.microsoft.com/en-us/library/edzehd2t(v=vs.110).aspx。 - Ron Beyer
2
谢谢你的提示(有时候在我的SO狂热中,我会忘记指南),我已经更新了我的代码。 - keenthinker

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

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Hello World");
        var num = new Number();
        num.numberChanged +=(s,e) =>{
            Console.WriteLine("Value was changed to {0}",num.Value); // in the demo below you can find another implementation for this sample using custom events
        };
        num.Value=10;
        num.Value=100;
    }
}

public class Number{
    public event EventHandler numberChanged;
    private int _value=0;
    public int Value
    {
        get{
            return _value;
        }
        set{
            if(value!=_value){
                _value=value;
                if(numberChanged!=null)
                    numberChanged(this,null);
            }
        }
    }
}

解释:

由于EventHandler委托有两个参数(sender,eventArgs),如here所述,您需要传递这些参数,我将它们作为se传递。

另一种订阅此事件的方法如下:

var num = new Number();
num.numberChanged += NumberChanged_Event; // below is the delegate method

public void NumberChanged_Event(object sender,EventArgs e)
{
   // your code goes here
}

我更新了演示以使用您自己的委托来传递旧值和新值,这可以在许多情况下有所帮助。

这里是一个可工作的 演示


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接