检查委托是否为空。

13

我正在阅读Essential C# 3.0这本书,想知道这种检查委托是否为空的方式是否好:

class Thermostat
{
    public delegate void TemperatureChangeHandler ( float newTemperature );

    public TemperatureChangeHandler OnTemperatureChange { get; set; }

    float currentTemperature;

    public float CurrentTemperature
    {
        get { return this.currentTemperature; }
        set
        {
            if ( currentTemperature != value )
            {
                currentTemperature = value;

                TemperatureChangeHandler handler = OnTemperatureChange;

                if ( handler != null )
                {
                    handler ( value );
                }
            }
        }
    }
}

如果类型是不可变的,解决方案是否会改变?我想也许使用不可变性,您就不会遇到这个线程问题。


谢谢,我没有仔细考虑,只是觉得这可能有助于这个线程案例。 - Joan Venge
6个回答

23

使用问号进行条件访问:

OnTemperatureChange?.Invoke();

空值条件运算符是短路的。也就是说,如果在一个条件成员或元素访问操作链中,有一个操作返回 null,那么链的其余部分不会执行。

2
仅适用于C# 6或更高版本。 - Matheus Rocha
2
C# 6于2016年发布,您可以简单升级。 - Luca Ziegler
2
我知道,那只是一个警告。并不是所有的环境都支持它。(例如Unity就不支持) - Matheus Rocha
3
Unity现在已经支持C# 7超过一年了。 ? 运算符适用于非Unity对象-因此在委托的情况下,它应该是完全支持的。 - S. Buda

15

原始(有些不准确)回答:

这个问题已经被广泛讨论。

简而言之:即使通过复制/检查null/执行步骤来做这件事,你也不能保证处理程序是有效的。

问题在于,如果在你复制它和执行复制之间,OnTemperatureChange被注销了,那么很可能你并不希望监听器被执行。

你可能还不如直接这样做:

if (OnTemperatureChange != null )
{
    OnTemperatureChange ( value );
}

处理空引用异常。

有时候我会添加一个什么也不做的默认处理程序,只是为了防止空引用异常,但这会严重影响性能,特别是在没有其他处理程序注册的情况下。

2014年7月10日更新:

我听从Eric Lippert的建议。

我的最初回答提到使用默认处理程序,但我没有像这篇文章中那样推荐使用临时变量,现在我同意这种做法也是好习惯。


4
如果您手动编写事件的添加/移除代码并处理同步,那么您可以保证不会出现问题。否则,多线程代码应处理可能出现的异常情况。 - Sam Saffron
3
是的,多线程代码在这方面确实很难正确地处理。请参见:https://dev59.com/8HRA5IYBdhLWcg3w6SNH - Sam Saffron
2
事件处理程序应该优雅地处理即使在取消注册后仍被调用的情况。只要委托持有对其的引用,事件处理程序所在的对象也不会被垃圾回收。简而言之,在创建一个不可变的事件委托副本并检查其是否为空后,完全可以安全地执行该副本。任何由此引起的错误都是处理程序的责任,必须进行纠正。 - Monstieur
1
@jnm2 我认为Eric Lippert对同样问题的回答更加清晰明了?http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx - John Weldon
1
@S.Buda 我已经修改了它。 - StayOnTarget
显示剩余2条评论

5

一个原因,使得你提供的代码比C.Ross版本更受推荐。然而,John也是正确的,如果事件在此期间被注销,则仍会存在另一个问题。我链接的博客建议处理程序确保即使在注销后也能被调用。


4

首先,您实际上并没有发布一个事件 - 因此,目前您的代码存在被人完全搞砸的风险。应该改为:

public event TemperatureChangeHandler CurrentTemperatureChanged;

“CurrentTemperatureChanged”这个名称对于数据绑定非常重要(有一个约定,运行时会使用给定的属性Foo来查找FooChanged)。然而,在我看来,这应该只是一个普通的“EventHandler”。数据绑定将寻找“EventHandler”,但更重要的是:您实际上没有在事件中提供任何信息,订阅者只需查看“obj.CurrentTemperature”即可获得所需信息。
我将用“TemperatureChangeHandler”来解释剩余的答案,但我鼓励您(再次)切换到“EventHandler”。
public event EventHandler CurrentTemperatureChanged;

方法:

TemperatureChangeHandler handler = CurrentTemperatureChanged;
if(handler != null) handler(value);

这是一个合理的做法,但是 (根据其他回复) 有一定的风险,即有些呼叫者可能会认为他们已经断开连接,但实际上并没有。不过这种情况很少发生。

另一种方法是使用扩展方法:

public static class TemperatureChangeExt {
    public static void SafeInvoke(this TemperatureChangeHandler handler,
             float newTemperature) {
        if (handler != null) handler(newTemperature);
    }
}

然后在你的类中,你可以直接使用以下内容:
        if (currentTemperature != value) {
            currentTemperature = value;
            CurrentTemperatureChanged.SafeInvoke(value);
        }

3
如果Thermostat类不需要线程安全,那么上述代码是可以的——只要有一个线程访问该Thermostat实例,就没有办法在测试null和调用事件之间取消注册OnTemperatureChange。

如果您需要使Thermostat线程安全,则可能需要查看以下文章(对我来说是新的,看起来很不错):

http://www.yoda.arachsys.com/csharp/events.html

就记录而言,建议您开发的类不要是线程安全的,除非明确需要线程安全,因为这可能会显著增加代码的复杂性。


开发线程安全并不会增加复杂性。例如,不可变对象在任何情况下通常都是一个好主意,并且它们始终是线程安全的。 - Rune FS

1

我只看到一些需要重构的地方,但除此之外看起来很不错...

class Thermostat
{
    public delegate void TemperatureChangeHandler ( float newTemperature );

    public TemperatureChangeHandler OnTemperatureChange { get; set; }

    float currentTemperature;

    public float CurrentTemperature
    {
        get { return this.currentTemperature; }
        set
        {
                if (currentTemperature != value)
                {
                        currentTemperature = value;

                        if (this.OnTemperatureChange != null )
                        {
                                this.OnTemperatureChange.Invoke( value );
                        }
                }
        }
    }
}

有趣...我以前没有见过这种方法。为什么要使用.Invoke()? - John Weldon
谢谢。这个没有其他人提到的问题吗? - Joan Venge
没有临时变量,这可能会导致空指针异常。问题在于,在检查后,OnTemperatureChange 可能会变成 null。请参见 http://blogs.msdn.com/ericlippert/archive/2009/04/29/events-and-races.aspx。 - Matthew Flaschen
@J.13.L 6 你可以使用方法语法调用委托方法,所以只需这样做: this.OnTemperatureChange(value); - Rune FS
1
是的,你陷入了两难境地。如果将其复制到临时变量中并检查是否为空,你可能会发现注销自己的侦听器不再处于接收事件的状态,真是个难题,我看不出有好的解决办法。 - jasonh
显示剩余2条评论

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