事件 - 命名约定和风格

75

我正在学习C#中的事件/委托。您能否就我选择的命名/编码样式发表意见(取自《Head First C#》书籍)?

明天我要向一个朋友讲解这个,我试图想出最优雅的方式来解释这些概念。(认为理解一个主题的最好方法是尝试教授它!)

class Program
    {
        static void Main()
        {
            // setup the metronome and make sure the EventHandler delegate is ready
            Metronome metronome = new Metronome();

            // wires up the metronome_Tick method to the EventHandler delegate
            Listener listener = new Listener(metronome);
            metronome.OnTick();
        }
    }

public class Metronome
    {
        // a delegate
        // so every time Tick is called, the runtime calls another method
        // in this case Listener.metronome_Tick
        public event EventHandler Tick;

        public void OnTick()
        {
            while (true)
            {
                Thread.Sleep(2000);
                // because using EventHandler delegate, need to include the sending object and eventargs 
                // although we are not using them
                Tick(this, EventArgs.Empty);
            }
        }
    }

public class Listener
    {
        public Listener(Metronome metronome)
        {
            metronome.Tick += new EventHandler(metronome_Tick);
        }

        private void metronome_Tick(object sender, EventArgs e)
        {
            Console.WriteLine("Heard it");
        }
    }

n.b. 代码重构自 http://www.codeproject.com/KB/cs/simplesteventexample.aspx

7个回答

109

微软实际上编写了广泛的命名指南,并将其放在 MSDN 库中。你可以在这里找到文章:命名指南

除了一般的大写规范外,该页面类型成员的名称还有关于“事件”的内容:

✔️ 使用动词或短语作为事件的名称。

例如:Clicked、Painting、DroppedDown等。

✔️ 为事件名称提供现在时和过去时概念,使用现在时和过去时。

例如,关闭窗口前引发的关闭事件应被称为 Closing,而在窗口关闭后引发的事件应被称为 Closed。

❌ 不要使用“Before”或“After”前缀或后缀来表示前后事件。请按照刚才描述的使用现在时和过去时。

✔️ 将事件处理程序(用作事件类型的委托)命名为“EventHandler”后缀,如下面的示例所示:

public delegate void ClickedEventHandler(object sender, ClickedEventArgs e);

✔️ 应在事件处理程序中使用名为sendere的两个参数。

sender参数表示引发事件的对象。通常情况下,sender参数的类型为object,即使可以使用更具体的类型。

✔️ 应该使用“EventArgs”后缀来命名事件参数类。


我应该使用过去式还是过去分词?OnShowed / OnShown? - Denis535

65

我想提几点:

Metronome.OnTick的命名似乎不太准确。从语义上看,“OnTick”告诉我它会在“Tick”时调用,但实际情况并非如此。我会将其命名为“Go”。

然而,通常接受的模型是这样做的。 OnTick 是一个虚方法,用于引发事件。这样,您可以轻松地在继承类中覆盖默认行为,并调用基类来引发事件。

class Metronome
{
    public event EventHandler Tick;

    protected virtual void OnTick(EventArgs e)
    {
        //Raise the Tick event (see below for an explanation of this)
        var tickEvent = Tick;
        if(tickEvent != null)
            tickEvent(this, e);
    }

    public void Go()
    {
        while(true)
        {
            Thread.Sleep(2000);
            OnTick(EventArgs.Empty); //Raises the Tick event
        }
    }
}
此外,我知道这只是一个简单的例子,但如果没有附加任何侦听器,您的代码将在Tick(this, EventArgs.Empty)上抛出异常。您至少应该包含一个空值防护来检查是否有侦听器:
if(Tick != null)
    Tick(this, EventArgs.Empty);

然而,在多线程环境中,如果在保护和调用之间取消注册监听器,则仍然存在漏洞。最好的方法是首先捕获当前的监听器并调用它们:

var tickEvent = Tick;
if(tickEvent != null)
    tickEvent(this, EventArgs.Empty);

我知道这是一个旧的回答,但因为它仍在得到赞,这里介绍一下使用 C# 6 的方法。整个“保卫”概念可以用条件方法调用来替换,而编译器确实对于捕获侦听器方面处理得非常正确:

Tick?.Invoke(this, EventArgs.Empty);

14
Guard(守卫)的替代方案是在Tick声明中添加“= delegate {};"(参见https://dev59.com/5HVC5IYBdhLWcg3woSpW#231536)。 - Benjol
请注意,此漏洞并不仅限于多线程环境。如果一个事件处理程序删除了事件中的所有处理程序,则在处理程序完成并且事件调用尝试运行下一个(现在不存在的)事件时,可能会导致崩溃。 - Greg D
@GregD:有没有什么方法可以让它变得傻瓜化,以至于客户端代码无法这样做? - Kyle Baran
3
@Kyle:嗯,我想我之前那条评论是不正确的。我觉得没必要太在意。 - Greg D
1
@GregD 情境在以下链接中进行了调查: https://blogs.msdn.microsoft.com/ericlippert/2009/04/29/events-and-races/ https://weblogs.asp.net/infinitiesloop/the-event-handler-that-cried-wolf - Wouter

27

我认为总体上包括命名规范在内最好的事件指南是这里

简要地说,我采用了以下惯例:

  • 事件名称通常以以-ing或-ed结尾的动词结束(例如Closing/Closed, Loading/Loaded)。
  • 声明事件的类应该有一个受保护的虚拟On [EventName]方法,这个方法应该被类的其余部分用于引发事件。该方法也可以被子类用于引发事件,并且可以重载以修改事件引发逻辑。
  • 经常会困惑如何使用“Handler”的问题-出于一致性的考虑,所有委托都应该后缀为Handler,尽量避免将实现处理程序的方法称为“handlers”。
  • 实现处理程序的默认VS命名约定是EventPublisherName_EventName。

7

4

在使用 .Net 中的事件多年后,我发现一个重要的问题是每次调用事件都需要重复检查事件处理程序是否为空。然而,我从未见过任何实际代码不调用空事件的情况。

因此,我开始在创建每个事件时放置一个虚拟处理程序,以避免进行空检查。

public class Metronome
{
    public event EventHandler Tick += (s,e) => {};

    protected virtual void OnTick(EventArgs e)
    {
        Tick(this, e);  // now it's safe to call without the null check.
    }
}

1
需要指出的一件事是,这不适用于序列化。空委托的想法在https://dev59.com/b-o6XIcBkEYKwwoYTzRZ#9282中得到了充分的讨论。 - sourcenouveau
1
它甚至可以更简单:public event EventHandler Tick = delegate {}; - Mikhail Poda

3

看起来不错,除了OnTick不遵循典型的事件调用模型。通常,On[EventName]只会引发事件一次,例如

protected virtual void OnTick(EventArgs e)
{
    if(Tick != null) Tick(this, e);
}

考虑创建这个方法,并将您现有的“OnTick”方法重命名为“StartTick”,而不是直接从StartTick调用Tick,请从StartTick方法中调用OnTick(EventArgs.Empty)

2
在您的情况下,可能是这样的:
class Metronome {
  event Action Ticked;

  internalMethod() {
    // bla bla
    Ticked();
  }
}

上面的示例使用以下约定,自我描述;]
事件源:
class Door {

  // case1: property change, pattern: xxxChanged
  public event Action<bool> LockStateChanged;

  // case2: pure action, pattern: "past verb"
  public event Action<bool> Opened;

  internalMethodGeneratingEvents() {
    // bla bla ...

    Opened(true);
    LockStateChanged(false);
  }

}

顺便提一下,关键字event是可选的,但它使“事件”与“回调”区分开来

事件监听器:

class AlarmManager {

  // pattern: NotifyXxx
  public NotifyLockStateChanged(bool state) {
    // ...
  }

  // pattern: [as above]      
  public NotifyOpened(bool opened) {
  // OR
  public NotifyDoorOpened(bool opened) {
    // ...
  }

}

绑定 [代码看起来更加人性化]

door.LockStateChanged += alarmManager.NotifyLockStateChanged;
door.Moved += alarmManager.NotifyDoorOpened;

即使手动发送事件,也是“人类可读的”。
alarmManager.NotifyDoorOpened(true);

有时候更加表达得清晰的方式是使用“动词+ing”的形式。
dataGenerator.DataWaiting += dataGenerator.NotifyDataWaiting;

无论您选择哪种约定,请始终保持一致。

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