如何将 List<T> 初始化为给定大小(而不是容量)?

Posted

技术标签:

【中文标题】如何将 List<T> 初始化为给定大小(而不是容量)?【英文标题】:How to initialize a List<T> to a given size (as opposed to capacity)? 【发布时间】:2009-01-21 20:54:23 【问题描述】:

.NET 提供了一个通用列表容器,其性能几乎相同(请参阅数组与列表的性能问题)。但是,它们在初始化方面完全不同。

数组很容易用默认值初始化,并且根据定义它们已经具有一定的大小:

string[] Ar = new string[10];

这允许人们安全地分配随机项目,例如:

Ar[5]="hello";

使用列表的事情更加棘手。我可以看到两种进行相同初始化的方法,这两种方法都不是你所说的优雅:

List<string> L = new List<string>(10);
for (int i=0;i<10;i++) L.Add(null);

string[] Ar = new string[10];
List<string> L = new List<string>(Ar);

什么是更清洁的方法?

编辑:到目前为止的答案是指容量,这与预先填充列表不同。例如,在刚刚创建的容量为 10 的列表上,不能执行 L[2]="somevalue"

编辑 2:人们想知道我为什么要以这种方式使用列表,因为这不是它们的预期使用方式。我可以看到两个原因:

    人们可以非常有说服力地争辩说,列表是“下一代”数组,增加了灵活性,几乎没有任何损失。因此,应该默认使用它们。我指出它们可能不那么容易初始化。

    我目前正在编写的是一个基类,它提供默认功能作为更大框架的一部分。在我提供的默认功能中,列表的大小是预先知道的,因此我可以使用数组。但是,我想为任何基类提供动态扩展它的机会,因此我选择了一个列表。

【问题讨论】:

"编辑:到目前为止的答案是指容量,这与预先填充列表不同。例如,在刚刚创建的容量为 10 的列表上,不能执行 L[2 ]="somevalue"" 鉴于此修改,也许您应该改写问题标题... 但是,用空值预填充列表有什么用,因为这就是 topicstarter 试图做的事情? 如果位置映射如此重要,那么使用 Dictionary 不是更有意义吗? List 不能替代 Array。它们解决了明显不同的问题。如果你想要一个固定的大小,你想要一个Array。如果您使用List,那么您做错了。 我总是发现那些试图在诸如“我不明白为什么我需要......”之类的论点中激怒的答案。它只意味着:你看不到它。它不一定意味着其他任何东西。我尊重人们想要提出更好的解决问题的方法,但应该更谦虚地表达,例如“你确定你需要一份清单吗?也许如果你告诉我们更多关于你的问题......”。通过这种方式,它变得愉快、引人入胜,并且鼓励 OP 改进他们的问题。成为赢家 - 保持谦虚。 【参考方案1】:
List<string> L = new List<string> ( new string[10] );

【讨论】:

我个人认为这是最干净的方式 - 尽管在问题正文中提到它可能很笨拙 - 不知道为什么 +1:刚刚遇到这样一种情况,我需要在通过索引填充之前使用一组固定的空值初始化可变大小的列表(然后添加额外的,因此数组不合适)。这个答案得到了我的投票,因为它既实用又简单。 绝妙的答案。 @GoneCoding我只是想知道新初始化的列表L在内部是否会简单地重用string[10]的内存(列表的后备存储也是一个数组),或者它会分配自己的新内存然后复制string[10]的内容?如果L 选择后面的路由,string[10] 将自动收集垃圾。 @RBT 这是这种方法的唯一缺点:数组将被重用,因此您将暂时拥有数组的两个副本(List 在内部使用数组,因为您似乎知道)。在极少数情况下,如果您有 大量 元素或内存限制,这可能会成为问题。是的,一旦 List 构造函数完成,额外的数组就有资格进行垃圾收集(否则这将是一个可怕的内存泄漏)。请注意,合格并不意味着“立即收集”,而是下次运行垃圾收集时。 这个答案分配了 10 个新字符串 - 然后根据需要遍历它们并复制它们。如果您正在使用大型数组;那么甚至不要考虑这个,因为它需要的内存是接受的答案的两倍。【参考方案2】:

我不能说我经常需要这个 - 你能详细说明你为什么需要这个吗?我可能会把它作为一个静态方法放在一个辅助类中:

public static class Lists

    public static List<T> RepeatedDefault<T>(int count)
    
        return Repeated(default(T), count);
    

    public static List<T> Repeated<T>(T value, int count)
    
        List<T> ret = new List<T>(count);
        ret.AddRange(Enumerable.Repeat(value, count));
        return ret;
    

可以使用Enumerable.Repeat(default(T), count).ToList(),但由于缓冲区大小调整,这样做效率低。

请注意,如果T 是引用类型,它将存储为value 参数传递的引用的count 副本-因此它们都将引用同一个对象。这可能是也可能不是您想要的,具体取决于您的用例。

编辑:如 cmets 中所述,您可以让 Repeated 使用循环来填充列表。那也会稍微快一点。就我个人而言,我发现使用 Repeat 的代码更具描述性,并怀疑在现实世界中性能差异无关紧要,但您的里程可能会有所不同。

【讨论】:

我意识到这是一篇旧帖子,但我很好奇。根据链接的最后一部分 (dotnetperls.com/initialize-array),与 for 循环相比,Enumerable.Repeat 的表现要差得多。根据 msdn,AddRange() 也具有 O(n) 复杂性。使用给定的解决方案而不是简单的循环是不是有点适得其反? @Jimmy:这两种方法都是 O(n),我发现这种方法更能描述我想要实现的目标。如果您喜欢循环,请随意使用它。 @Jimmy:另请注意,基准测试使用的是Enumerable.Repeat(...).ToArray(),这不是我使用它的方式。 我以下列方式使用了Enumerable.Repeat()(实现Pair,就像C++等价物一样):Enumerable.Repeat( new Pair&lt;int,int&gt;(int.MaxValue,-1), costs.Count)注意到副作用,List充满了引用副本到单个对象。更改像myList[i].First = 1 这样的元素会更改整个List 中的每个元素。我花了几个小时才找到这个错误。你们知道这个问题的任何解决方案吗(除了只使用公共循环并使用.Add(new Pair...) @Pac0,我刚刚在下面添加了一个答案。修改可能会被拒批。【参考方案3】:

使用将 int(“容量”)作为参数的构造函数:

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

编辑:我应该补充一点,我同意 Frederik。您使用 List 的方式与一开始使用它的全部理由背道而驰。

EDIT2:

编辑 2:我目前正在编写的是一个基类,它提供默认功能作为更大框架的一部分。在我提供的默认功能中,列表的大小是预先知道的,因此我可以使用数组。但是,我想为任何基类提供动态扩展它的机会,因此我选择了一个列表。

为什么有人需要知道所有空值的 List 的大小?如果列表中没有实际值,我希望长度为 0。无论如何,这很笨拙的事实表明它违背了该类的预期用途。

【讨论】:

这个答案不会在列表中分配 10 个空条目(这是要求),它只是为 10 个条目分配空间在需要调整列表大小之前(即容量),所以就问题而言,这与new List&lt;string&gt;() 没有什么不同。不过,在获得这么多赞成票方面做得很好:) 那个重载的构造函数是“初始容量”值而不是“大小”或“长度”,它也不初始化项目 回答“为什么有人需要这个”:我现在需要这个用于深度克隆树数据结构。一个节点可能会或可能不会填充其子节点,但我的基节点类需要能够使用它的所有子节点克隆自身。呸呸呸,那就更复杂了。 但我需要通过list[index] = obj; 填充我仍然为空的列表并使用其他一些列表功能。 @Bitterblue:此外,任何类型的预分配映射,您可能没有预先获得所有值。肯定有用途;我在经验不足的日子里写了这个答案。【参考方案4】:

先创建一个包含所需项目数的数组,然后将该数组转换为 List。

int[] fakeArray = new int[10];

List<int> list = fakeArray.ToList();

【讨论】:

【参考方案5】:

如果你想用某个固定值的 N 个元素来初始化列表:

public List<T> InitList<T>(int count, T initValue)

  return Enumerable.Repeat(initValue, count).ToList();

【讨论】:

请参阅 John Skeet 的回答,了解通过这种技术调整缓冲区大小的问题。【参考方案6】:

如果你想用一个固定的值来初始化它,你为什么要使用一个 List 呢? 我可以理解——为了性能——你想给它一个初始容量,但是列表相对于常规数组的优势之一不是它可以在需要时增长吗?

当你这样做时:

List<int> = new List<int>(100);

您创建一个容量为 100 个整数的列表。这意味着在您添加第 101 个项目之前,您的列表不需要“增长”。 列表的底层数组将被初始化为长度为 100。

【讨论】:

"如果你想用一个固定的值初始化它,为什么要使用 List" 好点。 他要求一个列表,其中每个元素都被初始化并且列表具有大小,而不仅仅是容量。这个答案现在是不正确的。 这个问题需要一个答案,而不是被问到的批评。有时这样做是有原因的,而不是使用数组。如果对为什么会出现这种情况感兴趣,可以向 Stack Overflow 提问。【参考方案7】:

您似乎在强调需要与您的数据建立位置关联,那么关联数组不是更合适吗?

Dictionary<int, string> foo = new Dictionary<int, string>();
foo[2] = "string";

【讨论】:

这是在回答与被问者不同的问题。【参考方案8】:

像这样初始化列表的内容并不是列表的真正用途。列表旨在保存对象。如果您想将特定数字映射到特定对象,请考虑使用哈希表或字典等键值对结构来代替列表。

【讨论】:

这并没有回答问题,而只是给出了一个不被问到的理由。【参考方案9】:

接受的答案(带有绿色复选标记的那个)有问题。

问题:

var result = Lists.Repeated(new MyType(), sizeOfList);
// each item in the list references the same MyType() object
// if you edit item 1 in the list, you are also editing item 2 in the list

我建议更改上面的行以执行对象的副本。关于这个有很多不同的文章:

String.MemberwiseClone() method called through reflection doesn't work, why? https://code.msdn.microsoft.com/windowsdesktop/CSDeepCloneObject-8a53311e

如果要使用默认构造函数而不是 NULL 来初始化列表中的每个项目,请添加以下方法:

public static List<T> RepeatedDefaultInstance<T>(int count)
    
        List<T> ret = new List<T>(count);
        for (var i = 0; i < count; i++)
        
            ret.Add((T)Activator.CreateInstance(typeof(T)));
        
        return ret;
    

【讨论】:

这不是“问题”——这是复制引用的正常行为。在不了解情况的情况下,您无法知道是否适合复制对象。问题不在于是否应该用副本填充列表 - 它是关于初始化具有容量的列表。我将根据它确实具有的行为来澄清我的答案,但我真的不认为在这个问题的背景下它算作问题。 @JonSkeet,我正在回复静态助手,这对于原始类型但不是引用类型是可以的。使用相同的引用项初始化列表的情况似乎不正确。在那种情况下,如果其中的每个项目都指向堆上的同一个对象,为什么还要有列表。 示例场景:您想要填充字符串列表,最初为每个元素添加一个“未知”条目,然后修改特定元素的列表。在这种情况下,所有“未知”值都引用同一个字符串是完全合理的。每次都克隆字符串是没有意义的。在一般意义上,使用对同一对象的多个引用填充字符串不是“正确”或“错误”。只要读者知道行为是什么,就由他们来决定它是否符合他们的特定用例【参考方案10】:

您可以使用 Linq 巧妙地使用默认值初始化您的列表。 (类似于David B's answer。)

var defaultStrings = (new int[10]).Select(x => "my value").ToList();

再进一步,用不同的值“string 1”、“string 2”、“string 3”等初始化每个字符串:

int x = 1;
var numberedStrings = (new int[10]).Select(x => "string " + x++).ToList();

【讨论】:

【参考方案11】:
string [] temp = new string[] "1","2","3";
List<string> temp2 = temp.ToList();

【讨论】:

List temp2 = new List(temp);就像 OP 已经建议的那样。 Ed - 这个确实回答了这个问题 - 这与容量无关。 但是 OP 已经声明他不喜欢这个解决方案。【参考方案12】:

关于 IList 的通知: MSDN IList Remarks: “IList 实现分为三类:只读、固定大小和可变大小。(...)。有关此接口的通用版本,请参阅 System.Collections.Generic.IList&lt;T&gt;。”

IList&lt;T&gt; 不继承自IList(但List&lt;T&gt; 确实实现了IList&lt;T&gt;IList),但始终是variable-size。 从 .NET 4.5 开始,我们也有 IReadOnlyList&lt;T&gt; 但 AFAIK,没有固定大小的通用列表,这将是您正在寻找的。​​p>

【讨论】:

【参考方案13】:

这是我用于单元测试的示例。我创建了一个类对象列表。然后我使用 forloop 添加了我期望从服务中获得的“X”个对象。 这样您就可以为任何给定大小添加/初始化列表。

public void TestMethod1()
    
        var expected = new List<DotaViewer.Interface.DotaHero>();
        for (int i = 0; i < 22; i++)//You add empty initialization here
        
            var temp = new DotaViewer.Interface.DotaHero();
            expected.Add(temp);
        
        var nw = new DotaHeroCsvService();
        var items = nw.GetHero();

        CollectionAssert.AreEqual(expected,items);


    

希望对大家有所帮助。

【讨论】:

【参考方案14】:

如果我理解正确,您需要新 T[size] 的 List 版本,而不需要向其添加值的开销。

如果你不怕List的实现在未来会发生巨大的变化(而且这种情况下我相信概率接近0),你可以使用反射:

    public static List<T> NewOfSize<T>(int size) 
        var list = new List<T>(size);
        var sizeField = list.GetType().GetField("_size",BindingFlags.Instance|BindingFlags.NonPublic);
        sizeField.SetValue(list, size);
        return list;
    

请注意,这考虑了底层数组的默认功能,以预填充项目类型的默认值。所有 int 数组的值为 0,所有引用类型数组的值为 null。另请注意,对于引用类型列表,仅创建指向每个项目的指针的空间。

如果您出于某种原因决定不使用反射,我希望提供一个带有生成器方法的 AddRange 选项,但在 List 下只调用了无数次 Insert,这不起作用。

我还要指出,Array 类有一个名为 ResizeArray 的静态方法,如果你想反过来从 Array 开始。

最后,我真的很讨厌当我问一个问题时,每个人都指出这是一个错误的问题。也许是这样,感谢您提供的信息,但我仍然想要一个答案,因为您不知道我为什么要问它。话虽这么说,如果你想创建一个对资源进行最佳利用的框架,List 是一个非常低效的类,除了在集合末尾保存和添加内容。

【讨论】:

【参考方案15】:

这是一个老问题,但我有两个解决方案。一是快速而肮脏的反射;另一种是实际回答问题的解决方案(设置大小而不是容量 em>) 同时仍然表现出色,而这里的答案都没有。


反射

这又快又脏,代码的作用应该很明显。如果你想加快速度,缓存GetField的结果,或者创建一个DynamicMethod来做:

public static void SetSize<T>(this List<T> l, int newSize) =>
    l.GetType().GetField("_size", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(l, 10);

显然很多人会犹豫是否将这样的代码投入生产。


ICollection&lt;T&gt;

此解决方案基于构造函数List(IEnumerable&lt;T&gt; collection) 优化ICollection&lt;T&gt; 并立即将大小调整到正确的数量,而不对其进行迭代。然后它调用集合CopyTo 进行复制。

代码如下:

public List(IEnumerable<T> collection) 
....
    ICollection<T> c = collection as ICollection<T>;
    if (collection is ICollection<T> c)
    
        int count = c.Count;
        if (count == 0)
        
            _items = s_emptyArray;
        
        else 
            _items = new T[count];
            c.CopyTo(_items, 0);
            _size = count;
        
        

因此,我们可以完全优化地将 List 预初始化为正确的大小,而无需任何额外的复制。

怎么会?通过创建一个除了返回Count 之外什么都不做的ICollection&lt;T&gt; 对象。具体来说,我们将CopyTo 中实现任何东西,这是唯一调用的其他函数。

private class SizeCollection<T> : ICollection<T>

    public SizeCollection(int size) =>
        Count = size;

    public void Add(T i)
    public void Clear()
    public bool Contains(T i)=>true;
    public void CopyTo(T[]a, int i)
    public bool Remove(T i)=>true;
    public int Count get;
    public bool IsReadOnly=>true;
    public IEnumerator<T> GetEnumerator()=>null;
    IEnumerator IEnumerable.GetEnumerator()=>null;


public List<T> InitializedList<T>(int size) =>
    new List<T>(new SizeCollection<T>(size));

理论上,我们可以对AddRange/InsertRange 对现有数组执行相同的操作,这也解释了ICollection&lt;T&gt;,但是那里的代码为假定的项目创建一个新数组,然后将它们复制进去。在这种情况下,空循环 Add 会更快:

public void SetSize<T>(this List<T> l, int size)

    if(size < l.Count)
        l.RemoveRange(size, l.Count - size);
    else
        for(size -= l.Count; size > 0; size--)
            l.Add(default(T));

【讨论】:

【参考方案16】:

有点晚了,但你提出的第一个解决方案对我来说似乎更干净:你没有分配内存两次。 甚至 List 构造器也需要遍历数组才能复制它;它甚至不知道里面只有空元素。

1。 - 分配 N - 循环 N 成本:1 * allocate(N) + N * loop_iteration

2。 - 分配 N - 分配N+循环() 成本:2 * allocate(N) + N * loop_iteration

但是 List 的分配循环可能会更快,因为 List 是一个内置类,但 C# 是 jit 编译的所以...

【讨论】:

以上是关于如何将 List<T> 初始化为给定大小(而不是容量)?的主要内容,如果未能解决你的问题,请参考以下文章

是否可以将 XML 反序列化为 List<T>?

如何将 JSON 反序列化为具有多个 List<T> 参数的 .NET 对象?

将 JSON 数组反序列化为 List<T> C# 时出错

如何将 Kotlin 的 MutableList 初始化为空 MutableList?

将 JSON 反序列化为 List<List<KeyValuePair>> 结果为空

如何将项目添加到 List<T> 的开头?