返回null还是空List?

有这样的一个interface:

public interface Filter
{
    IList<string> Filter(IList<string> responses)
}

在实现它时,如果过滤后没有结果,返回null还是new List()?

一段流水线处理代码

先看下面一段流水线处理的内部方法,即GenerateCandidates(), FilterA()和FilterB()都没有用户输入等边界:

IList<string> GenerateResponses()
{
    List<string> candidates = GenerateCandidates();
    if (candidates == null || !candidates.Any())
    {
        return null;
    }

    List<string> filteredByA = FilterA(candidates);
    if (filteredByA == null || !filteredByA.Any())
    {
        return null;
    }
    
    List<string> filteredByB = FilterB(filteredByA);
    if (filteredByB == null || !filteredByB.Any())
    {
        return null;
    }
    ...
}

对于这种流水线作业,如果返回值是个Collection,到处检查返回值是否为null是一种非常恶心和多余的做法,不符合简洁代码的原则,并增加了许多无效操作。和下面的代码比较,哪一段更简洁、易读和不易出错?

IList<string> GenerateResponses(IList<string> responses)
{
    List<string> candidates = GenerateCandidates();
    List<string> filteredByA = FilterA(candidates);
    List<string> filteredByB = FilterB(filteredByA);
    ...
}

在系统中引入null是一颗老鼠屎坏了一锅粥,只要一处返回了null,那么在整个调用栈中都需要到处检查中间结果是否为null,否则就可能引起nullreferenceexception,系统处理异常甚至崩溃。

先找出两段支持返回空List的说法:

Don't Return Null

Clean Code: A Handbook of Agile Software Craftsmanship Chapter 7 Error Handling

DO NOT return null values from collection properties or from methods returning collections. Return an empty collection or an empty array instead. Users of collection properties often assume that the following code will always work: IEnumerable list = GetList(); foreach (string name in list) { ... }

Framework Design Guidelines 2nd EditionPage. 256

几种边界情况

关于返回null还是empty list是有许多争议的,stackoverflow上甚至有一些讨论因为争议过大而被关闭了。但我倾向于返回empty list因为nothing to lose。 ## 性能问题? 分两种情况讨论: ### new List()的开销 return null或new List()的开销差异在99.99%以上的场景都可以直接忽略,如果真的在意性能,反倒是在每处检查是否为null或Any()对效率的影响更多。 如果返回值是IEnumberable,则可返回Enumerable.Empty()提升些效率。

短路返回以提升性能,不必做后续处理

就上例而言,假设FilterA()的实现开销较大比如是远程调用,那么是否可以在调用FilterA()之前判断它若为空则提前返回?

  1. 如果在意处理的性能,那么更好的做法应该是在FilterA()内部判断输入是否为空,若为空则直接返回而不进行后续处理,在FilterA()中判断是由抽象层次所决定的更干净的做法:

    IList<string> FilterA(IList<string> responses)
    {
        if (!responses.Any())
        {
            return response;
        }
        ...
    }

  2. 正常流程中即使response只有一个元素,那么它也会走完整个流程,所以可以认为走完GenerateResponses()的流程是可以接受的,那么既然正常流程的处理效率可以接受,处理一个空列表的效率更可接受

防御编程?

防御编程(Defensive programming)是一个非常好的概念,但在这里的防御是多此一举,或者说是过度防御。此处有隐含假设:

  1. GenerateResponses()仅由内部使用,而非外部API。若是后者,那么应当在输入边界进行条件判断
  2. “写让机器懂的代码容易,写让人懂的代码难”,易读性和可维护性是代码需要首要考虑的因素

Tech lead应该在coding convention中强制这种情况下返回Collection,code review时也应将此问题抛出。

错误处理?

任何时候都不返回null吗?话虽不能说绝,但在绝大多数情况下,如果输入或中间结果出现了null,则应该按exception来处理,直接抛异常是更为干净和自然的做法,远胜于在整个调用栈中返回null并一直检查的做法。

结论

在返回值是Collection时,尽量返回空容器而不是null。

Reference

Is it better to return null or empty collection?