遍历sequence,然后调用Count(),或者先创建一个List,然后调用Count

本文关键字:Count 调用 然后 一个 List sequence 遍历 或者 创建 | 更新日期: 2023-09-27 18:19:13

我使用的语言是c#

假设我们要迭代一个名为customers的序列中的元素,该序列是一个虚构类型的对象序列Customer。在代码方面,让我们有以下内容:

IEnumerable<Customer> customers = module.GetCustomers();

其中module是一个服务层的类,通过它的一个方法,我们可以检索所有的客户。在此前提下,通过customers元素的迭代将是:

foreach(var customer in customers)
{
}

现在让我们在遍历customers的元素后得到客户的数量。可以这样做:

int numberOfCustomers = customers.Count();

我现在关心的/问题是:

使用Count()方法,我们再次迭代customers的元素。但是,如果我们已经创建了这些对象的内存集合,例如调用方法ToList():

List<Customer> customers = module.GetCustomers()
                                 .ToList();

我们将使用列表customersCount属性获得O(1)中的客户数量。

为了找出这两个选项之间,哪一个是最好的,我写了一个简单的控制台应用程序,我使用StopWatch类来分析它们。然而,我没有得到一个明确的结果。

这两个选项中哪一个是最好的?

我运行了以下控制台应用程序:

class Program
{
    static void Main(string[] args)
    {
        IEnumerable<int> numbers = Enumerable.Range(0, 1000);
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        foreach (var number in numbers)
            Console.WriteLine(number);

        Console.WriteLine(numbers.Count());
        stopwatch.Stop();
        // I got 175ms
        Console.WriteLine(stopwatch.ElapsedMilliseconds);
        Console.ReadKey();
        stopwatch.Restart();
        List<int> numbers2 = numbers.ToList();
        foreach (var number in numbers2)
            Console.WriteLine(number);
        Console.WriteLine(numbers2.Count);
        stopwatch.Stop();
        // I got 86ms
        Console.WriteLine(stopwatch.ElapsedMilliseconds);
        Console.ReadKey();          
    }
}

然后我运行这个:

class Program
{
    static void Main(string[] args)
    {
        IEnumerable<int> numbers = Enumerable.Range(0, 1000);
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        List<int> numbers2 = numbers.ToList();
        foreach (var number in numbers2)
            Console.WriteLine(number);
        Console.WriteLine(numbers2.Count);
        stopwatch.Stop();
        // I got 167ms
        Console.WriteLine(stopwatch.ElapsedMilliseconds);
        Console.ReadKey();
        stopwatch.Restart();
        foreach (var number in numbers)
            Console.WriteLine(number);

        Console.WriteLine(numbers.Count());
        stopwatch.Stop();
        // I got 104ms
        Console.WriteLine(stopwatch.ElapsedMilliseconds);
        Console.ReadKey();          
    }
}

遍历sequence,然后调用Count(),或者先创建一个List,然后调用Count

我通常更喜欢让我的存储库方法返回一个IReadOnlyCollection<>,这有助于调用者知道他们可以安全地迭代它多次:

IReadOnlyCollection<Customer> customers = module.GetCustomers();

如果我不能这样做,并且我知道我要对给定的内容进行多次迭代,我通常会使用。tolist()来确保我正在处理内存中的集合:

var customers = module.GetCustomers().ToList();

在客户已经是一个内存集合的情况下,通过创建一个列表会增加一点开销,但它有助于避免通过多次从数据库中检索数据来创建大量开销的风险。

您的基准测试有几个原因,但最大的原因之一是它使用Console.WriteLine(),它执行I/O操作。该操作所花费的时间将远远超过迭代集合和计算结果的总和。事实上,在Console.WriteLine()中花费的时间的差异将超过您正在测试的代码中的差异。

但这实际上很好地说明了我的观点——I/O操作比CPU和内存操作花费的时间要长得多,所以添加.ToList()通常是值得的,这可能会增加运行时间的微秒,以避免添加I/O操作的最小可能性,这可能会增加毫秒。