如何以及何时放弃在C#中使用数组?

我总是被告知,向数组添加元素的方式如下:

创建数组+ 1element的空副本,然后将原始数组中的数据复制到其中,然后加载新元素的新数据

如果这是真的,那么由于内存和CPU利用率的原因,在需要大量元素活动的场景中使用数组是正确的,对吗?

如果是这种情况,你是否应该尽量避免在添加大量元素时尽可能多地使用数组? 你应该使用iStringMap吗? 如果是这样,如果您需要两个以上的维度并且需要添加大量元素添加,会发生什么。 你是刚刚受到性能打击还是应该使用其他东西?

查看通用List作为数组的替代。 它们支持大多数数组相同的function,包括根据需要分配初始存储大小。

这实际上取决于“添加”的含义。

如果你的意思是:

 T[] array; int i; T value; ... if (i >= 0 && i <= array.Length) array[i] = value; 

然后,不,这不会创建一个新数组,实际上是改变.NET中任何类型的IList的最快方法。

但是,如果您使用的是ArrayList,List,Collection等,那么调用“Add”方法可能会创建一个新数组 - 但是他们对它很聪明,它们不只是调整1个元素,它们几何增长,所以如果你每隔一段时间只添加很多值就必须分配一个新的数组。 即使这样,如果您知道要添加多少元素,也可以使用“容量”属性强制它list.Capacity += numberOfAddedElementslist.Capacity += numberOfAddedElements

一般来说,我更喜欢避免使用数组。 只需使用List 。 它在内部使用动态大小的数组,并且对于大多数用途来说足够快。 如果您正在使用多维数组,请使用List >>。 它在内存方面并没有那么糟糕,并且添加项目要简单得多。

如果您处于需要极速的0.1%的使用率,请确保在您尝试优化之前确实是您的列表访问。

如果您要添加/删除元素很多,只需使用List。 如果它是多维的,您可以始终使用List >或其他东西。

另一方面,如果您主要执行的操作是遍历列表,则列表的效率低于数组,因为数组全部位于CPU缓存中的一个位置,其中列表中的对象遍布整个位置。

如果您想使用数组进行有效读取,但是您要经常“添加”元素,则有两个主要选项:

1)将其生成为列表(或列表列表),然后使用ToArray()将其转换为有效的数组结构。

2)将数组分配为比您需要的更大,然后将对象放入预先分配的单元格中。 如果您最终需要的元素数量超过预先分配的数量,则可以在数组填充时重新分配数组,每次都会增加一倍。 这给出了O(log n)resize的性能而不是O(n),就像重新分配一次添加数组一样。 请注意,这几乎是StringBuilder的工作原理,为您提供了一种更快的方式来连续追加字符串。

什么时候放弃使用数组

  1. 首先, 当数组的语义与你的意图匹配时 – 需要一个动态增长的集合? 一套不允许重复的套装? 一个必须保持不变的集合? 在所有情况下都避免使用数组。 这是99%的案例。 只是陈述明显的基本观点。

  2. 其次, 当你没有编写绝对性能关键性时 – 大约95%的情况。 数组的边缘性能更好 , 特别是在迭代中 。 它几乎永远不会重要。

  3. 当你没有params关键字的参数强迫时 – 我只是希望params接受任何IEnumerable或甚至更好的语言构造本身来表示序列 (而不是框架类型)。

  4. 当您编写遗留代码或处理互操作时

简而言之,您实际上需要一个arrays非常罕见。 我会补充为什么可以避免它?

  1. 避免数组imo的最大原因是概念性的。 数组更接近实现,更远离抽象。 arrays传达了更多的方式,而不是违背高级语言精神的方式。 这并不奇怪,考虑到arrays更接近金属,它们直接来自特殊类型(尽管内部数组是一个类)。 不是教学,但数组确实转化为很少需要的语义。 最有用和最频繁的语义是具有任何条目的集合,具有不同项目的集合,键值映射等,具有可添加,只读,不可变,顺序相关变体的任何组合。 考虑一下,你可能想要一个可添加的集合,或只有预定义项目的只读集合,无需进一步修改,但你的逻辑看起来像“我想要一个动态可添加的集合,但只有固定数量的集合,它们也应该是可修改的“? 我会说非常罕见。

  2. Array是在pre-generics时代设计的,它模仿了许多运行时黑客的通用性,它会在这里和那里显示它的怪异。 我找到的一些渔获物:

    1. 破坏的协方差。

       string[] strings = ... object[] objects = strings; objects[0] = 1; //compiles, but gives a runtime exception. 
    2. 数组可以为您提供结构参考! 。 这与其他地方不同。 一个样品:

       struct Value { public int mutable; } var array = new[] { new Value() }; array[0].mutable = 1; //<-- compiles ! //a List[0].mutable = 1; doesnt compile since editing a copy makes no sense print array[0].mutable // 1, expected or unexpected? confusing surely 
    3. 运行时实现的方法如ICollection.Contains对于结构和类可以是不同的 。 这不是什么大问题,但是如果你忘记为参考类型正确覆盖非genericsEquals ,期望generics集合寻找genericsEquals ,你将得到不正确的结果。

       public class Class : IEquatable { public bool Equals(Class other) { Console.WriteLine("generic"); return true; } public override bool Equals(object obj) { Console.WriteLine("non generic"); return true; } } public struct Struct : IEquatable { public bool Equals(Struct other) { Console.WriteLine("generic"); return true; } public override bool Equals(object obj) { Console.WriteLine("non generic"); return true; } } class[].Contains(test); //prints "non generic" struct[].Contains(test); //prints "generic" 
    4. T[]上的Length属性和[]索引器似乎是可以通过reflection访问的常规属性(这应该涉及一些魔法),但是当涉及到表达式树时,你必须吐出与编译器完全相同的代码。 有ArrayLengthArrayIndex方法可以单独完成。 这里有一个问题 。 另一个例子:

       Expression> e = () => new[] { "a" }[0]; //e.Body.NodeType == ExpressionType.ArrayIndex Expression> e = () => new List() { "a" }[0]; //e.Body.NodeType == ExpressionType.Call; 

如何放弃使用数组

最常用的替代品是List ,它具有更干净的API。 但它是一个动态增长的结构,这意味着您可以在末尾添加List或插入任何容量的任何位置。 没有什么可以替代数组的确切行为,但人们大多使用数组作为只读集合,你不能在其末尾添加任何东西。 替代品是ReadOnlyCollection 。 我带这个扩展方法:

 public ReadOnlyCollection ToReadOnlyCollection(IEnumerable source) { return source.ToList().AsReadOnly(); } 

调整数组大小时,必须分配新数组,并复制内容。 如果您只是修改数组的内容,那只是一个内存赋值。

因此,如果您不知道数组的大小,或者大小可能会发生变化,则不应使用数组。 但是,如果您有一个固定长度的数组,它们是一种通过索引检索元素的简单方法。

ArrayList和List在需要时将数组增加多个(我认为是通过加倍大小,但我没有检查源)。 在构建动态大小的数组时,它们通常是最佳选择。

当您的基准测试表明数组resize会严重降低您的应用程序的速度时(请记住 – 过早优化是所有恶意的根源),您可以评估使用调整的resize行为编写自定义数组类。

通常,如果您必须具有BEST索引查找性能,则最好首先构建List,然后将其转换为数组,从而首先支付小额罚款但后来避免使用。 如果问题是您将不断添加新数据并删除旧数据,那么您可能希望使用ArrayList或List以方便使用,但请记住它们只是特殊情况的数组。 当他们“成长”时,他们会分配一个全新的arrays并将所有内容复制到其中,这非常慢。

ArrayList只是一个在需要时增长的数组。 添加是分摊O(1),只是要小心确保resize不会发生在糟糕的时间。 插入是O(n)必须移动右侧的所有项目。 删除是O(n)必须移动右侧的所有项目。

同样重要的是要记住List不是链表。 它只是一个类型化的ArrayList。 List 文档确实注意到它在大多数情况下表现更好,但没有说明原因。

最好的办法是选择适合您问题的数据结构。 这取决于很多事情,因此您可能希望浏览System.Collections.Generic命名空间。

在这种特殊情况下,我会说,如果你能想出一个好的关键值, 词典将是你最好的选择。 它具有接近O(1)的插入和移除。 但是,即使使用Dictionary,也必须注意不要让它调整内部数组的大小(O(n)操作)。 最好通过在构造函数中指定更大,然后期望使用的初始容量来为它们提供大量空间。

-Rick

应使用长度定义标准数组,该长度保留连续块中所需的所有内存。 将项添加到数组会将其放入已保留内存块中。

对于少数写入和许多读取,数组非常有用,特别是那些具有迭代性质的读取 – 对于其他任何内容,使用许多其他数据结构之一。

你是对的,arrays非常适合查找。 然而,对arrays尺寸的修改是昂贵的。

您应该在要修改数组大小的方案中使用支持增量大小调整的容器。 您可以使用允许您设置初始大小的ArrayList,并且可以不断检查大小与容量,然后通过大块增加容量以限制resize的数量。

或者您可以使用链接列表。 然而,看起来很慢……

关于各种数组类型的效率,这个论坛post可能会或可能没有用处: C#数组 – 多维vs词典

如果我认为我将在其生命周期中大量添加项目,那么我将使用List。 如果我确定在声明它时集合的大小是多少,那么我将使用一个数组。

另一次我通常在List上使用数组是当我需要将一个集合作为对象的属性返回时 – 我不希望调用者通过List的Add方法添加集合的项目,而是希望他们将项目添加到集合中通过我的对象的界面。 在这种情况下,我将获取内部List并调用ToArray并返回一个数组。

如果您要进行大量添加, 并且您不会进行随机访问(例如myArray[i] )。 您可以考虑使用链表( LinkedList ),因为它永远不会像List实现那样“增长”。 但请记住,您只能使用IEnumerable接口真正访问LinkedList实现中的项目。

您可以做的最好的事情是尽可能预先分配尽可能多的内存。 这将阻止.NET进行额外调用以获取堆上的内存。 如果失败那么分配五个或任何数量的块对你的应用程序有意义是有意义的。

这是一个你可以真正应用于任何事情的规则。