如何将 List < T > 初始化为给定的大小(相对于容量) ?

.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. 我目前正在编写的是一个基类,它提供默认功能,作为更大框架的一部分。在我提供的默认功能中,List 的大小在高级中已知,因此我可以使用数组。但是,我想为任何基类提供动态扩展它的机会,因此我选择了一个列表。

283893 次浏览

使用带有 int (容量)的构造函数作为参数:

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

编辑: 我应该补充说,我同意弗雷德里克。你使用清单的方式违背了最初使用清单的整个理由。

编辑2:

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

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

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

当你这样做的时候:

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

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

像这样初始化列表的内容并不是列表的真正用途。列表是用来保存对象的。如果您想要将特定的数字映射到特定的对象,可以考虑使用键-值对结构,如散列表或字典,而不是列表。

我不能说我经常需要这个——你能详细解释一下你为什么想要这个吗?我可能会将它作为一个静态方法放在 helper 类中:

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副本-因此它们都将引用同一个对象。这可能是您想要的,也可能不是,这取决于您的用例。

编辑: 正如注释中提到的,如果愿意,可以让 Repeated使用一个循环来填充列表。这样也会快一点。就我个人而言,我发现使用 Repeat的代码更具描述性,并且怀疑在现实世界中性能差异将是无关紧要的,但是您的经验可能会有所不同。

string [] temp = new string[] {"1","2","3"};
List<string> temp2 = temp.ToList();

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

public List<T> InitList<T>(int count, T initValue)
{
return Enumerable.Repeat(initValue, count).ToList();
}

你似乎在强调数据位置关联的必要性,那么关联数组不是更合适吗?

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

可以使用 Linq 巧妙地用默认值初始化列表(类似于 David 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();

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

int[] fakeArray = new int[10];


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

关于 IList 的通知: MSDN IList 备注 : ”IList 实现分为三类: 只读、固定大小和可变大小 System.Collections.Generic.IList<T>

IList<T>不继承自 IList(但 List<T>同时实现了 IList<T>IList) ,但总是 可变尺寸。 自从。NET 4.5,我们也有 IReadOnlyList<T>,但 AFAIK,没有固定大小的通用列表,这将是你所寻找的。

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

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);




}

希望我能帮到你们。

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

1. - 分配 N 循环 N 开销: 1 * 分配(N) + N * 循环 _ 迭代

2. - 分配 N - 分配 N + 循环() 开销: 2 * 分配(N) + N * 循环 _ 迭代

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

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

问题是:

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

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

如果你想用缺省构造函数而不是空来初始化列表中的每一项,那么添加以下方法:

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;
}

经过反复思考,我已经找到了 OP 问题的非反思式答案,但是 Charlieface 抢先了一步。所以我相信正确而完整的答案是 https://stackoverflow.com/a/65766955/4572240

我的老答案是:

如果我理解正确的话,您希望使用 List < T > version of new T [ size ] ,而不需要为其添加值。

如果您不担心 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 > 对于任何事情来说都是一个非常低效的类,而不是在集合的末尾保存和添加内容。

这是一个古老的问题,但我有两个解决办法。一个是快速而肮脏的反射; 另一个是解决方案,即 真正回答了问题(设置为 大小 而不是 容量)仍然有效,这里的答案都没有这样做。


反射

这样做既快捷又简单,代码的作用应该非常明显。如果希望加快速度,可以缓存 GetField 的结果,或者创建 DynamicMethod:

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

显然,很多人对于将这样的代码投入生产会犹豫不决。


ICollection<T>

这个解决方案基于这样一个事实: 构造函数 List(IEnumerable<T> collection)ICollection<T>进行了优化,并立即将大小调整到正确的数量,而不需要对其进行迭代。然后它调用集合 CopyTo来完成复制。

List<T>构造函数的代码如下:

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>对象,该对象除了返回一个 Count以外什么也不做。具体来说,我们将 实现 CopyTo中的任何内容,这是唯一被调用的其他函数。

private struct 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<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));
}