null对象与空对象

时间:2009-10-27 02:20:10

标签: c# c++ null

[这是Best Practice: Should functions return null or an empty object?的结果,但我想要非常一般。 ]

在我见过的许多遗留(生产)C ++代码中,有一种倾向于写很多 NULL (或类似的) )检查测试指针。在添加 NULL 时,许多这些都会在发布周期结束时添加 - 检查可以快速修复指针取消引用导致的崩溃 - 并且没有太多时间进行调查

为了解决这个问题,我开始编写带有(const)引用参数的代码,而不是传递指针的(更多)常见技术。没有指针,不想检查 NULL (忽略实际上有空引用的极端情况)。

在C#中,存在相同的C ++“问题”:希望检查针对nullArgumentNullException)的每个未知引用,并通过添加{{来快速修复NullReferenceException 1}}检查。

在我看来,防止这种情况的一种方法是首先使用空对象(nullString.Empty)来避免空对象。另一种方法是抛出异常,而不是返回EventArgs.Empty

我刚刚开始学习F#,但看起来在那个环境中的空对象要少得多。那么也许你真的不需要有很多null引用?

我在这里咆哮错误的树吗?

11 个答案:

答案 0 :(得分:25)

传递非null只是为了避免NullReferenceException正在交换一个简单,易于解决的问题(“它因为它为空而爆炸”),这是一个更加微妙,难以调试的问题(“多次调用的东西”堆栈下行的行为并不像预期的那样,因为早些时候它有一些没有有意义信息但没有空的对象“)。

NullReferenceException是精彩的东西!它失败,大声,快速,几乎总是快速,容易地识别和修复。这是我最喜欢的例外,因为我知道当我看到它时,我的任务只需要大约2分钟。将此与令人困惑的质量保证或客户报告进行对比,试图描述必须复制并追溯到原点的奇怪行为。呸。

这一切都归结为你作为一种方法或一段代码可以合理地推断出你的代码。如果你被提交了一个空引用,并且你可以合理地推断出调用者可能的意思是null(例如,可能是一个空集合?)那么你绝对应该只处理空值。但是,如果您无法合理地推断如何处理null,或者调用者的意思是null(例如,调用代码告诉您打开文件并将该位置赋值为null),则应抛出ArgumentNullException

在每个“网关”点维护这样的正确编码实践 - 代码中功能的逻辑边界 - NullReferenceExceptions应该更加罕见。

答案 1 :(得分:7)

我倾向于怀疑具有大量NULL的代码,并尝试在可能的情况下将它们重构为异常,空集合等等。

Martin Fowler的Refactoring(第260页)中的“引入空对象”模式也可能有所帮助。 Null对象响应真实对象的所有方法,但是以“做正确的事”的方式。因此,不要总是检查Order以查看order.getDiscountPolicy()是否为NULL,请确保Order在这些情况下具有NullDiscountPolicy。这简化了控制逻辑。

答案 2 :(得分:4)

Null得到我的投票。再说一次,我是'快速失败'的心态。

String.IsNullOrEmpty(...)也非常有用,我想它可以捕获这两种情况:null或空字符串。您可以为您传递的所有课程编写类似的函数。

答案 3 :(得分:3)

如果您编写的代码返回null作为错误条件,那么请不要:通常,您应该抛出异常 - 更难以错过。

如果您正在使用您担心可能返回null的代码,那么大部分都是boneheaded exceptions:也许在调用者处进行一些Debug.Assert检查以在开发期间检测输出。您不应该确实在生产中需要大量null次检查,但是如果某些第三方库以不可预测的方式返回大量null个,那么肯定:做检查。

在4.0中,您可能希望查看代码合同;这让你有更好的控制来说“这个参数永远不应该作为null传递”,“这个函数永远不会返回null”等等 - 让系统在静态分析期间(即你构建时)验证这些声明。

答案 4 :(得分:2)

关于null的事情是它没有意义。它只是缺少一个物体。

所以,如果你真的是指一个空字符串/集合/什么,总是返回相关对象而不是null。如果有问题的语言允许您指定,请执行此操作。

如果您想要返回的内容不是静态类型可指定的值,那么您有许多选项。返回null是一个答案,但没有意义有点危险。抛出异常可能实际上就是你的意思。您可能希望扩展具有特殊情况的类型(可能具有多态性,即特殊情况模式(其特殊情况是空对象模式))。您可能希望将返回值包装在具有更多含义的类型中。或者您可能想传入一个回调对象。通常有很多选择。

答案 5 :(得分:1)

我说这取决于。对于返回单个对象的方法,我通常会返回null。对于返回集合的方法,我通常会返回一个空集合(非null)。不过,这些更符合指导方针而非规则。

答案 6 :(得分:1)

如果您认真想要在“null更少”的环境中进行编程,请考虑更频繁地使用扩展方法,它们不受NullReferenceExceptions的影响,至少“假装”null不是那里:

public static GetExtension(this string s)
{
    return (new FileInfo(s ?? "")).Extension;
}

可以称为:

// this code will never throw, not even when somePath becomes null
string somePath = GetDataFromElseWhereCanBeNull();
textBoxExtension.Text = somePath.GetExtension(); 

我知道,这只是方便而且许多人正确地认为它违反了OO原则(尽管OO的“创始人”Bertrand Meyer认为null邪恶并完全将其从他的OO设计中消除,这是应用于埃菲尔语,但这是另一个故事)。编辑:丹提到比尔瓦格纳(更有效的C#)认为这是不好的做法,他是对的。曾经考虑过IsNull扩展方法;-)?

为了使代码更具可读性,可能会有另一个提示:当对象为null时,更频繁地使用null-coalescing运算符来指定默认值:

// load settings
WriteSettings(currentUser.Settings ?? new Settings());

// example of some readonly property
public string DisplayName
{
    get 
    {
         return (currentUser ?? User.Guest).DisplayName
    }
}

这些都不会偶尔检查null(而??只不过是一个隐藏的if分支)。我希望代码尽可能少null,因为我相信它使代码更具可读性。当我的代码被null的if语句弄得乱七八糟时,我知道设计中存在一些错误并且我重构了。我建议任何人都这样做,但我知道这个问题的意见差异很大。

(更新)与例外的比较

到目前为止,讨论中没有提到与异常处理的相似性。当你发现自己无处不在时忽略null,只要你认为它在你的方式,它基本上与写作相同:

try 
{
    //...code here...
}
catch (Exception) {}

具有删除任何异常跟踪的效果,只是为了找到它会在代码中引发不相关的异常。虽然我认为避免使用null是好的,如前面提到的那样,在异常情况中为null是好的。只是不要将它们隐藏在null-ignore-blocks中,它最终会产生与catch-all-exceptions块相同的效果。

答案 7 :(得分:1)

对于例外主角,他们通常源于交易编程和强大的异常安全保证或盲目指导。在任何体面的复杂性,即。异步工作流,I / O,特别是网络代码,他们根本不合适。您之所以在C ++中看到关于此问题的Google样式文档,以及所有良好的异步代码“没有强制执行”(还想想您最喜欢的托管池)。

还有更多内容,虽然它可能看起来像一个简化,但它确实很简单。对于其他人来说,你会得到很多例外,这些例外并非设计用于繁重的例外使用..无论如何我离题,从世界顶级图书馆设计师那里阅读,通常的地方是提升(只是不要混淆它)与另一个喜欢异常的阵营,因为他们必须写音乐软件: - )。

在你的实例中,这不是Fowler的专业知识,由于可用的转换机制(可能但肯定不是总是通过 dominance ),因此只能在C ++中实现高效的“空对象”习惯用法。另一方面,在您的null 类型中,您可以在保留干净的调用站点和代码结构的同时抛出异常并执行任何操作。

在C#中,您的选择可以是一个好的或错误的类型的单个实例;因此,它能够投掷接受或简单地按原样运行。因此,它可能会或可能不会违反其他合同(取决于您所面临的代码质量,您认为哪种更好)。

最后,它确实清理了调用站点,但是不要忘记你将面临与许多库的冲突(特别是从容器/词典返回,结束迭代器,以及任何其他'接口'代码到外面的世界)。 null-as-value检查是极其优化的机器代码片段,需要记住,但我会同意任何一天狂野指针使用而不理解constness,引用和更多将导致不同类型的可变性,别名和性能问题

要添加,没有银弹,并且在空引用上崩溃或在托管空间中使用空引用,或抛出和不处理异常是一个完全相同的问题,尽管托管和异常世界会试图向您推销。任何体面的环境都可以提供保护(你可以在你想要的任何操作系统上安装任何过滤器,你认为虚拟机还能做什么),而且还有很多其他的攻击媒介,这个被过分苛刻。再次从Google输入x86验证,他们自己做得更快更好'IL','动态'友好代码等方式。

本着你的直觉,权衡利弊和本地化效果..将来你的编译器将优化所有检查,并且比任何运行时或编译时人类方法更有效(但不是轻松进行跨模块交互)。

答案 8 :(得分:1)

我尽量避免从方法中返回null。通常有两种情况 - 当null结果合法时,以及何时不应该发生。

在第一种情况下,当没有结果是合法的时,有几种解决方案可用于避免空结果和与它们相关联的空检查:Null Object patternSpecial Case pattern是否返回替换对象什么都不做,或在特定情况下做某些具体事情。

如果没有返回任何对象是合法的,但是在Null对象或特殊情况方面仍然没有合适的替代品,那么我通常使用Option functional type - 我可以在没有时返回空选项法律结果。然后由客户决定什么是处理空选项的最佳方式。

最后,如果从方法返回任何对象是不合法的,只是因为如果缺少某些东西,该方法无法产生结果,那么我选择抛出异常并进一步执行。

答案 9 :(得分:0)

您不能总是返回一个空对象,因为并不总是定义'empty'。例如,int,float或bool为空是什么意思?

返回NULL指针不一定是坏习惯,但我认为返回(const)引用是更好的做法(当然这样做是有意义的。)

最近我经常使用Fallible类:

Fallible<std::string> theName = obj.getName();
if (theName)
{
    // ...
}

此类可以使用各种实现(请查看Google代码搜索),I also created my own

答案 10 :(得分:0)

空对象如何比空对象更好?你只是重命名症状。问题是你的函数的契约定义过于松散“这个函数可能返回一些有用的东西,或者它可能返回一个虚拟值”(其中虚拟值可能为null,一个“空对象”或一个魔术常量,如{ {1}}。但无论你如何表达这个虚拟值,呼叫者仍然必须在使用返回值之前检查它。

如果要清理代码,解决方案应该是缩小函数范围,使其首先不返回虚拟值。

如果你有一个可能返回一个值的函数,或者什么都不返回,那么指针就是一种表达它的常用(和有效)方法。但通常情况下,您的代码可以重构,以便消除这种不确定性。如果你能保证函数返回一些有意义的东西,那么调用者可以依赖它返回有意义的东西,然后他们就不必检查返回值。