145

.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:人们想知道为什么我要以这种方式使用列表,因为这不是它们的预期使用方式。我可以看到两个原因:

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

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

4

16 回答 16

167
List<string> L = new List<string> ( new string[10] );
于 2009-03-10T20:04:08.967 回答
96

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

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是引用类型,它将存储count为参数传递的引用的副本value- 因此它们都将引用同一个对象。这可能是也可能不是您想要的,具体取决于您的用例。

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

于 2009-01-21T21:00:19.217 回答
23

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

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

编辑:我应该补充一点,我同意弗雷德里克。您使用 List 的方式与一开始使用它的整个推理背道而驰。

编辑2:

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

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

于 2009-01-21T20:56:16.697 回答
11

首先创建一个包含您想要的项目数的数组,然后将该数组转换为列表。

int[] fakeArray = new int[10];

List<int> list = fakeArray.ToList();
于 2016-09-02T10:54:18.410 回答
8

如果要使用某个固定值的 N 个元素初始化列表:

public List<T> InitList<T>(int count, T initValue)
{
  return Enumerable.Repeat(initValue, count).ToList();
}
于 2009-01-21T21:24:23.550 回答
7

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

当你这样做时:

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

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

于 2009-01-21T20:57:37.677 回答
5

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

Dictionary<int, string> foo = new Dictionary<int, string>();
foo[2] = "string";
于 2009-01-23T13:30:50.633 回答
4

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

于 2009-01-21T20:59:30.310 回答
2

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

问题:

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

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

如果要使用默认构造函数而不是 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;
    }
于 2019-07-25T14:52:21.033 回答
1
string [] temp = new string[] {"1","2","3"};
List<string> temp2 = temp.ToList();
于 2009-01-21T21:05:17.613 回答
1

您可以使用 Linq 巧妙地使用默认值初始化您的列表。(类似于大卫 B 的回答。)

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();
于 2014-11-24T19:15:40.033 回答
0

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

IList<T>不继承自IList(但List<T>确实实现了IList<T>and IList),但始终是variable-size。从 .NET 4.5 开始,IReadOnlyList<T>除了 AFAIK 之外,我们还没有固定大小的通用列表,这正是您要寻找的。

于 2016-11-09T09:27:18.500 回答
0

这是我用于单元测试的示例。我创建了一个类对象列表。然后我使用 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);


    }

希望我对你们有帮助。

于 2016-11-20T15:48:43.137 回答
0

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

如果你不怕 List<T> 的实现在未来会发生巨大的变化(而且在这种情况下我相信概率接近于 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<T> 下仅调用 Insert 无数次,这不起作用。

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

最后,我真的很讨厌当我问一个问题时,每个人都指出这是一个错误的问题。也许是这样,感谢您提供的信息,但我仍然想要一个答案,因为您不知道我为什么要问它。话虽如此,如果您想创建一个对资源进行最佳利用的框架,List<T> 对于任何事情来说都是一个效率很低的类,而不是在集合的末尾保存和添加东西。

于 2020-09-12T13:35:38.523 回答
0

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


反射

这既快又脏,代码的作用应该很明显。如果你想加快速度,缓存 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<T>

该解决方案基于这样一个事实,即构造函数List(IEnumerable<T> collection)优化ICollection<T>并立即将大小调整到正确的数量,而无需对其进行迭代。然后它调用集合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 预初始化为正确的大小,而无需任何额外的复制。

怎么会这样?通过创建一个ICollection<T>除了返回 a 之外什么都不做的对象Count。具体来说,我们不会实现任何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/做同样的事情,这也解释了循环:InsertRangeICollection<T>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));
}
于 2021-01-17T23:13:10.500 回答
-1

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

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

2. - 分配 N - 分配 N + loop () 成本:2 * allocate(N) + N * loop_iteration

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

于 2016-11-30T01:09:50.983 回答