【问题标题】:copying a list to a new list - More efficient & Best Practice将列表复制到新列表 - 更高效和最佳实践
【发布时间】:2017-09-18 18:57:55
【问题描述】:

将列表复制到新列表的适当方法是什么? 将列表复制到新列表的最有效方法是什么?

通过效率,而不是代码效率,更多的是在幕后框架意义上。

List<String>List2 = List.ToList();

或者:

List<String>List2 = new List<String>();
foreach (string item in List)
{
 List2.Add(item);
}

更新:

更高效的 IL 代码呢?

【问题讨论】:

  • 好吧,您的第一个示例不必要地初始化了List2,然后丢弃了该初始化。应该只是List&lt;String&gt; List2 = List.ToList();
  • 你为什么不写一个测试并与我们分享结果?
  • 老实说,两者之间的性能差异会很小,我怀疑你会注意到。使用任何一种都可以使您的代码更易于阅读。
  • 你的名单有多大?每秒复制多少次?除了对这些问题的极端答案之外,任何给定答案之间的差异都是微不足道的。如果您对其中任何一个都有极端的答案,您可能需要考虑 List&lt;T&gt; 以外的其他设计或容器。 (编辑:我可能建议的唯一修改是在您的第二个示例中预先提供列表大小以避免数组大小调整,但同样,只有对于相对较大或大量频繁复制​​的列表才真正具有可衡量的效果)

标签: c#


【解决方案1】:

鉴于List&lt;T&gt; 有一个IEnumerable&lt;T&gt; 构造函数,我更喜欢这种形式:

List<string> newList = new List<string>(otherList);

编辑

正如 Ondrej 在下面的反编译代码中指出的那样,List&lt;T&gt; 的构造函数预先分配了数组的大小并将内容复制过来。这将比创建一个新列表然后迭代另一个列表单独添加项目要快得多,尤其是在您的第二个示例中,您没有指定要预分配多少项目。

【讨论】:

  • 如果你查看反编译的源代码,ToList 就是这样做的。
  • 我猜是因为你不需要遍历内容。
  • 出于“性能”,此构造函数是自动将新列表初始化为适当的长度,还是在传输元素时进行动态调整大小?
  • @ChrisSinclair 如果您在下面看到 Ondrej 的回答,它会使用传递的集合大小预分配数组。
  • @Casperah 在 List 构造函数中,它将IEnumerable&lt;T&gt; 转换为ICollection&lt;T&gt;,从而提前知道大小。
【解决方案2】:

ToList 的作用(缩短):

public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source)
{
    return new List<TSource>(source);
}

List ctor 的作用(缩短):

public List(IEnumerable<T> collection)
{
    ICollection<T> collection2 = collection as ICollection<T>;
    int count = collection2.Count;
    this._items = new T[count];
    collection2.CopyTo(this._items, 0);
    this._size = count;
}

所以 ToList() 效率更高 - 它确实首先分配空间,然后一步复制所有项目。

【讨论】:

  • ToList 如果继续调用 ctor,效率如何?
  • 因为它已经分配了新列表的大小来容纳所有项目。而第二种方法将它们一一列出。 List 不是动态的 - 一旦有更多的项目,它的容量就会被重新分配,这是昂贵的操作。
  • 其实我的错 - 我以为你在这里比较 ToListctor,但你不是。对不起!
【解决方案3】:

你可以使用List&lt;T&gt; constructor,它需要IEnumerable&lt;T&gt;

List<string> list1 = new List<string>();

// fill list1

List<string> list2 = new List<string>(list1);

【讨论】:

  • 如果你查看反编译的源代码,这就是 ToList 所做的。
  • 你不会一直看反编译的代码吧?我认为这是人们出于可读性目的而使用的。
  • 在这种特殊情况下,我正在寻找代码可读性之外的东西。
  • @oleksii 但是ToList 似乎很简单。我认为所有 cadrell0 都在说,就效率而言,鉴于ToList 正在做与您相同的事情,因此不会有任何区别。
【解决方案4】:

在效率方面,第一个会更快。 List&lt;T&gt; 的底层实现是一个 ArrayList,因此当您调用 .Add 时,您可能需要调整底层数组的大小。

另一方面,.ToList 可以确定新 List 的正确初始大小,并避免 foreach 技术遭受的重新分配操作。

考虑到这一点,我推荐.ToList。更少的代码,它会更快。

您可以运行一个简单的程序来验证ToList 确实更快:

void Main()
{
    List<int> items = new List<int>();

    items = Enumerable.Range(0, 1000000).ToList();

    CopyWithToList(items);
    CopyWithForeach(items);

}

public void CopyWithToList<T>(List<T> list) 
{
    var sw = Stopwatch.StartNew();
    List<T> copy = list.ToList();
    sw.Stop();
    Console.WriteLine("CopyWithToList: {0}", sw.Elapsed);
}

public void CopyWithForeach<T>(List<T> list)
{
    var sw = Stopwatch.StartNew();
    List<T> copy = new List<T>();
    foreach (T item in list) {
        copy.Add(item);
    }
    sw.Stop();

    Console.WriteLine("CopyWithForeach: {0}", sw.Elapsed);
}

【讨论】:

  • 即使您提供具有初始大小的列表以进行预分配,它仍然较慢。我怀疑这真的来自这样一个事实,即构造函数调用(ToList 使用)本质上执行了一个数组复制,而foreach 执行了检查集合更改、获取迭代器、迭代、调用时检查的所有额外开销.Add() 如果底层数组足够长,等等
【解决方案5】:

测试表明,最佳性能使用 .ToList() 方法(对于具有 21474836 个元素的列表,它在笔记本电脑 Core i5 CPU 上运行大约 48 毫秒)。

其他所有方法都比较慢,使用.Add()的方法最差,同时谈论性能。

这是一些测试代码:

class Program
{
    static void Main()
    {
        var list = new List<int>();

        for (int i = 0; i < int.MaxValue / 100; i++)
        {
            list.Add(i);
        }

        TimeItAccurate(ListCopy_1, list, 10);
        TimeItAccurate(ListCopy_2, list, 10);
        TimeItAccurate(ListCopy_3, list, 10);
        TimeItAccurate(ListCopy_4, list, 10);
        TimeItAccurate(ListCopy_5, list, 10);
    }

    private static List<int> ListCopy_1(List<int> list)
    {
        var newList = list.ToList();

        return newList;
    }

    private static List<int> ListCopy_2(List<int> list)
    {
        var newList = new List<int>(list.Count);

        foreach (var i in list)
        {
            newList.Add(i);
        }

        return newList;
    }

    private static List<int> ListCopy_3(List<int> list)
    {
        var newList = new List<int>(list.ToArray());

        return newList;
    }

    private static List<int> ListCopy_4(List<int> list)
    {
        var newList = new List<int>(list.Count);

        newList.AddRange(list);

        return newList;
    }

    private static List<int> ListCopy_5(List<int> list)
    {
        var newList = new List<int>(list);

        return newList;
    }

    public static void TimeItAccurate<TIn, TResult>(Func<TIn, TResult> func, TIn argument, int iterationsCount)
    {
        #region Pre-heat
        for (int i = 0; i < 10; i++)
        {
            var t = func.Invoke(argument);
        }
        #endregion

        var stopwatch = new Stopwatch();
        var result = default(TResult);

        stopwatch.Start();
        for (int i = 0; i < iterationsCount; i++)
        {
            result = func.Invoke(argument);
        }
        stopwatch.Stop();

        Console.WriteLine("Result:\n{4}(...) == {0}\n\n{1} iterations done in {2} ms.\nAverage time: {3:f5} ms.",
            result,
            iterationsCount,
            stopwatch.ElapsedMilliseconds,
            stopwatch.ElapsedMilliseconds / (double)iterationsCount,
            func.Method.Name);
    }
}

结果:

Result (.ToList()):
ListCopy_1(...) == System.Collections.Generic.List`1[System.Int32]

10 iterations done in 474 ms.
Average time: 47.40000 ms.

Result (for-cycle with .Add()):
ListCopy_2(...) == System.Collections.Generic.List`1[System.Int32]

10 iterations done in 1896 ms.
Average time: 189.60000 ms.

Result (ctor with .ToArray()):
ListCopy_3(...) == System.Collections.Generic.List`1[System.Int32]

10 iterations done in 981 ms.
Average time: 98.10000 ms.

Result (.AddRange()):
ListCopy_4(...) == System.Collections.Generic.List`1[System.Int32]

10 iterations done in 959 ms.
Average time: 95.90000 ms.

Result (new List<int>(list)):
ListCopy_5(...) == System.Collections.Generic.List`1[System.Int32]

10 iterations done in 480 ms.
Average time: 48.00000 ms.

【讨论】:

  • 您的第三次测试有缺陷。你为什么打电话给ToArray() - 直接传递列表。
  • 只是通过列表将是一样的,正如我们从this answer 知道的那样。我只是想展示一些变体如何进行复制操作。
  • 那么也许您可以考虑添加“直接到ctor”作为完整性的第五次测试。
  • 我想知道为什么 ToList 在内部使用 ctor 版本时平均快一秒?
  • 这也是我的问题。在 10 次迭代中运行 .ToList 有时会更慢,但是当我达到 100 和 1000 次迭代时,ctor 总是会失败。
【解决方案6】:

我相信这两个例子是相同的,.ToList() 可能是后者实现的。

最好的表现应该是这样的:

List<String> list2 = new List<String>(list.Count);
foreach(String item in list)
    list2.Add(item);

重要的部分是创建具有足够容量来容纳其内容的 list2。

如果您之后不需要修改任一列表,则您只需要参考副本:

List<String> list2 = list;

【讨论】:

  • 有更好的方法来实现这一点。
  • 重要的是通过在其构造函数中指定容量来为列表分配足够的空间。这样列表在添加项目时不需要重新分配列表。
猜你喜欢
  • 1970-01-01
  • 2016-03-06
  • 1970-01-01
  • 2015-08-21
  • 1970-01-01
  • 1970-01-01
  • 2015-03-14
  • 2019-07-10
  • 1970-01-01
相关资源
最近更新 更多