比 IList、ICollection 和 IEnumerable 快的列举列表方法

5

最近,我一直在研究函数返回集合的写法惯例。我想知道真正使用List<int>的函数是应该返回List<int>还是IList<int>ICollection<int>IEnumerable<int>。我对性能进行了一些测试,结果让我感到非常惊讶。

static List<int> list = MakeList();
static IList<int> iList = MakeList();
static ICollection<int> iCollection = MakeList();
static IEnumerable<int> iEnumerable = MakeList();

public static TimeSpan Measure(Action f)
{
    var stopWatch = new Stopwatch();
    stopWatch.Start();
    f();
    stopWatch.Stop();
    return stopWatch.Elapsed;
}

public static List<int> MakeList()
{
    var list = new List<int>();
    for (int i = 0; i < 100; ++i)
    {
        list.Add(i);
    }
    return list;
}

public static void Main()
{
    var time1 = Measure(() => { // Measure time of enumerating List<int>
        for (int i = 1000000; i > 0; i-- ) {
            foreach (var item in list)
            {
                var x = item;
            }
        }
    });
    Console.WriteLine($"List<int> time: {time1}");

    var time2 = Measure(() => { // IList<int>
        for (int i = 1000000; i > 0; i-- ) {
            foreach (var item in iList)
            {
                var x = item;
            }
        }
    });
    Console.WriteLine($"IList<int> time: {time2}");

    var time3 = Measure(() => { // ICollection<int>
        for (int i = 1000000; i > 0; i-- ) {
            foreach (var item in iCollection)
            {
                var x = item;
            }
        }
    });
    Console.WriteLine($"ICollection<int> time: {time3}");

    var time4 = Measure(() => { // IEnumerable<int>
        for (int i = 1000000; i > 0; i-- ) {
            foreach (var item in iEnumerable)
            {
                var x = item;
            }
        }
    });
    Console.WriteLine($"IEnumerable<int> time: {time4}");
}

输出:

List<int> time: 00:00:00.7976577
IList<int> time: 00:00:01.5599382
ICollection<int> time: 00:00:01.7323919
IEnumerable<int> time: 00:00:01.6075277

我尝试了不同的措施,包括让MakeList()返回上述接口之一,但所有尝试都只证明返回一个List<int>并将其作为List<int>处理,速度大约是使用接口的两倍。

然而,包括这个答案在内的各种来源都声称永远不应该返回List<>,而应该始终使用接口。

所以我的问题是:

  • 为什么处理List<int>大约比接口快两倍?
  • 从函数中返回什么以及如何管理代码以获得更好的性能?

1
经验法则:让输入参数尽可能通用:void Do(IEnumerable<int> source)void Do(List<int> source) 更好;返回值则相反:越具体,越好。List<int> Do()IEnumerable<int> Do() 更好。 - Dmitry Bychenko
1
这被称为https://en.wikipedia.org/wiki/Robustness_principle。 - GSerg
2
关于返回值,我认为它也不应该太具体,这样实现就有更多的空间进行更改(即返回不同的具体类型),而不会破坏调用方。 - Sweeper
1
@DmitryBychenko:我同意Sweeper的观点;你的建议是反向的。实现有前提条件,因此接受满足前提条件的最一般类型。调用者有要求,因此返回满足要求的最一般类型。这样可以为实现者和调用方提供最大的灵活性来做出选择。 - Eric Lippert
1
此外,在这种特定情况下,返回列表和返回序列之间存在语义差异。返回列表表示“请继续修改此内容;我已经给你一份副本”。返回序列表示“不要将其用作除序列以外的任何内容”。如果调用者需要副本,他们可以轻松地使用 ToList 制作一个。 - Eric Lippert
显示剩余2条评论
1个回答

9
为什么处理 List<int> 约快两倍于接口?
这是因为在尝试对某个东西进行 foreach 时,C#首先检查集合的类型是否已经有一个名为 GetEnumerator 的方法,该方法返回具有 MoveNextCurrent 的类型。如果有,它会直接调用它们。如果没有,则回退到使用 IEnumerable<T>IEnumerableIEnumerator<T>IEnumerator 来获取枚举器,以便调用 MoveNextCurrent
这种设计选择有两个原因。首先,在泛型之前的 C# 1.0 世界中,这意味着可以调用返回 intCurrent。当然,IEnumerator.Current 是对象,并将框住 int,这既是速度和内存的惩罚。其次,它意味着集合的作者可以做实验,找出哪种实现 MoveNextCurrent 的方式性能最好。 List<T> 的实现者确实做到了这一点;如果您检查 List<T> 上的 GetEnumerator,您将发现有趣的事情:它返回一个可变值类型。是的,可变值类型被认为是易于滥用的坏习惯。但由于这个重载的 GetEnumerator 中99.999%的用法是由 foreach 代表您调用的,所以大多数情况下,您甚至没有注意到有一个可变值可以被滥用,因此不会滥用它。
(注意:前面段落的要点不应该是“使用可变值类型因为它们很快”。要点应该是了解用户的使用模式,然后设计一个安全、高效的工具,以满足他们的需求。通常,可变值类型不是正确的工具。)
总之,长话短说,当迭代在编译时已知为 List<T> 时,我们通过直接绑定到可变值类型上的方法来避免所有类型的虚拟调用、接口类型检查等等。
什么应该从函数中返回,如果我们关心性能,如何管理代码?
如果你关心速度表现,那么你应该将注意力集中在程序中最慢的部分。如果你的程序中最慢的部分是在对集合调用 MoveNext,那么恭喜你,你有一个非常快的程序;MoveNext 是需要优化的下一个步骤。但在这种情况下,你应该问自己“如何完全避免或推迟这个循环?”。
如果 MoveNext 不是程序中最慢的部分,那么它在特定实现中慢几个纳秒又有什么关系呢?返回逻辑上最接近调用者所需的类型,不要担心微小的惩罚。

1
非常好的答案,您实际上指出了我担心的时间差异在循环执行更多操作时实际上是非常小的。 - Rasmond
返回逻辑上最接近调用者所需的类型。问题在于,如果您正在开发可供许多不同开发人员用于许多不同目的的库,那么很难知道或理解调用者的最佳类型。然而,即使您是调用者,特别是如果集合由多个调用链以不同方式使用,调用者的需求可能会在开发期间发生变化,因此您必须重构代码以返回不同且更有用的类型。 - Luca Cremonesi
1
@LucaCremonesi:如果你的评论论点是我们常常不知道调用者会如何使用方法或需要它做什么,那么不幸地是你是对的,但这可以更好地表述为“我们经常在真正理解我们要解决的问题之前开始编写代码”,我们应该停止这样做。 - Eric Lippert
有时候,我们今天要解决的问题与明天要解决的问题不同,因为需求和优先级会发生变化,新客户或现有客户提出了新的或不同的请求。这就是为什么越来越多地采用敏捷软件开发的原因。你不想发布一个解决一年前问题的产品;你想发布一个解决今天问题的软件。 - Luca Cremonesi

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