永远不要使用空值?

19

我们目前正在为C#编写一些编码规范。

我最近编写了一个具有以下签名的方法:

string GetUserSessionID(int UserID)

GetUserSession() 如果没有找到用户的会话,则返回 null。

在我的调用代码中...我这样说...

string sessionID = GetUserSessionID(1)
if (null == sessionID && userIsAllowedToGetSession)
{
   session = GetNewUserSession(1);
}

最近在代码审核中,审查员说“从方法中返回null会让调用方法需要检查空值,因此你不应该这样做。”

我立即哭诈骗,因为如果你返回string.Empty,你仍然需要对返回值执行某种检查。

if (string.Empty == sessionID)

然而,经过进一步思考,对于 Collection/Array/List 的情况,我永远不会返回 null。

我会返回一个空列表。

我认为解决方案是将其重构为两个方法。

bool SessionExists(int userID);

并且

string GetUserSessionID(int UserID);

这一次,GetUserSessionID()会抛出一个 SessionNotFound 异常(因为它不应该返回 null)

现在代码看起来像这样...

if(!SessionExists(1) && userIsAllowedToGetSession))
{
   session = GetNewUserSession(1);
}
else
{
   session = GetUserSessionID(1);
}

现在这意味着没有空值,但对我来说似乎有点复杂。这只是一个非常简单的例子,我想知道这会如何影响更复杂的方法。

有很多最佳实践建议关于何时抛出异常以及如何处理它们,但关于null使用的信息似乎较少。

还有其他人有任何关于使用null的可靠指南(甚至更好的标准),这对可空类型意味着什么(我们是否应该使用它们)?

提前感谢,

Chris。


谢谢大家!非常有趣的讨论。

我选择了egaga的答案,因为他们提供了Get vs Find作为编码指南的建议,但所有答案都很有趣。


如果您在整个过程中都知道当前用户的ID是什么,那么是否考虑使用CoordObj.Context.Current.Session和CoordObj.Context.Current.HasSession代替GetUserSessionID()函数呢? - Chris S
或者您可以让 GetUserSessionID 在必要时创建会话。这将减轻调用者的任务,并消除空值。 - Michael Kohne
在C#中,访问器并不好用。为什么不定义一个Users类,其中包含operator[]和SessionId属性呢?"Users[userId].SessionId"。 - Samir Talwar
@Chris/Frederick 实际上我不是前C++程序员。也许我应该开始学习它 :-)!! - ChrisV
15个回答

29

相比于“魔法值”,null更好,也就是说更加诚实。但是在发生错误时不应返回null-这就是异常的用途。当涉及到返回集合时... 我同意一个空的集合比null更好。


我认为异常应该仅用于不寻常的错误,而不是任何错误,例如网络连接已断开-抛出异常,没有匹配的搜索结果-返回一个指示这种可能发生的错误的变量(bool、null或其他)。 - morechilli

11

返回null是可以的,以下代码简洁易懂:

var session = GetUserSessionID(1) ?? GetNewUserSession(1);

8

很好,正是我想说的。当调用一个名为FindSomething的函数时,它肯定会让你想到需要处理找不到something的情况。 - Scott Langham

5
我认为您不应该排除使用null作为返回值。我认为在许多情况下这是有效的。但是,您应该仔细考虑每种方法的优缺点。它完全取决于方法的目的和调用者的期望。
我个人在许多情况下使用null返回值,例如当您可能期望方法不返回对象时(比如通过主键进行数据库搜索,可能返回一个或零个/ null)。当您可以正确地预期方法返回一个值时,应该使用异常。
在您的特定示例中,我认为它取决于系统的上下文。例如,如果调用只从您可能期望已登录的用户代码中进行,那么应该抛出异常。但是,如果没有用户登录并且因此没有会话ID可返回,则应选择返回null。

5

4

保持简单。让数据的使用者尽可能地感到轻松无痛。这是一种权衡:实现所需的时间和获得的收益。

  • 处理获取列表/集合之类的事情时,如你所指出的,返回一个空列表。
  • 同样地,在需要返回“null”值表示“未找到”时 - 尝试使用Null Object pattern作为经常使用的类型。对于很少使用的类型,我想你可以接受一些检查null或-1(String.IndexOf)。

如果您有更多类别,请在评论中发布,我将尝试提供解决方案。


3

我本人很谨慎地避免返回null,但在您的示例中,这似乎是正确的做法。

重要的是明确参数和返回值的可空性。如果您的语言无法直接表达此概念,则必须在API文档中进行说明。 (我已经读过Java不能,但C#可以)。如果输入参数或返回值可能为空,请确保告诉用户在API的上下文中这意味着什么。

我们最大的代码库是一个Java应用程序,它在过去的十年中稳步增长。许多内部API的行为与null相关非常不清楚。这会导致在所有地方都进行防御性编程而出现大量的null检查。

特别丑陋的是,返回集合的函数返回null而不是一个空集合。什么!?不要这样做!

在字符串的情况下,请确保区分必须存在但可以为空的字符串和真正可选的字符串(可以为null)。在使用XML时,我们经常遇到这种区别:强制存在但可以为空的元素与可选元素。

系统的一部分提供类似于XML的结构的查询以实现业务规则,并且极力避免使用null。它无处不用Null对象模式。在这种情况下,这么做很有用,因为它允许您执行以下操作:

A.children().first().matches("Blah")

这个表达式的意思是,如果A的第一个子节点命名为“Blah”,则应该返回true。如果A没有子节点,它不会崩溃,因为在这种情况下,first()返回实现了Node接口但从不“匹配”任何内容的NullNode。

总结:

  • 在API中明确空值的含义和允许性。
  • 动动脑筋。
  • 不要教条主义。

3

空值比魔法值更好,更有意义。你可以尝试编写一个 TryGetUserSession 方法。

bool TryGetUserSession(int sessionId, out session)

同时,请避免写 nulls == ???,因为某些开发人员可能会觉得难以阅读。

祝好!


2

一种解决方法可能是为这些东西声明一个简单的返回类型:

class Session
{
    public bool   Exists;
    public string ID;
}

Session GetUserSession(int userID) {...}

...

Session session = GetUserSessionID(1);

if (session.Exists)
{
    ... use session.ID
}

我通常情况下避免使用null,但是字符串是一个特殊的情况。我经常调用string.IsNullOrEmpty()。事实上我们甚至为string写了一个扩展方法:

static class StringExtensions
{
    public static bool IsNullOrEmpty(this string value)
    {
        return string.IsNullOrEmpty(value);
    }
}

那么您可以执行以下操作:
string s = ... something

if (s.IsNullOrEmpty())
{
 ...
}

另外,既然我们在谈论编码风格:

为什么要使用看起来不自然的 "if (null == ...)" 风格呢?对于C#来说完全没有必要 - 看起来像是有人将一些C++的东西带入了C#的编码风格中!


我真的想点赞这篇文章,因为它提到了IsNullOrEmpty;但是我很震惊你竟然要费这么大劲来避免调用静态类,并且还评论了“负担”。 - overslacked
我所提到的“包袱”与将C++习惯用法引入C#编码风格有关,与使用扩展方法并无太大关系!我最初看到IsNullOrEmpty()扩展是在Jon Skeet的提及中(也许你听说过他?)。这里稍微讨论了它的优缺点: https://dev59.com/CHRA5IYBdhLWcg3w4iJ_ - Matthew Watson
但是,普遍的共识是你不应该扩展方法,这样你就可以在空方法上调用它们。 :) - Matthew Watson
我理解你所指的内容;但是当你明确地测试一个现有对象实例的存在时,抱怨“反向”相等性测试看起来不自然,对我来说有点讽刺。无论如何,我认为你和ammoQ的答案是最好的(我们都有包袱,我也有自己学习/喜欢的做事方式!)。 - overslacked

2

我更喜欢返回空集合而不是null,因为这有助于避免像以下代码一样混乱的调用者代码:

if( list != null) {
    foreach( Item item in list ) {
         ...
    }
}

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