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

Posted

技术标签:

【中文标题】如何以及何时放弃在 C# 中使用数组?【英文标题】:How and when to abandon the use of arrays in C#? 【发布时间】:2010-09-09 17:04:45 【问题描述】:

我一直被告知向数组中添加元素是这样发生的:

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

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

如果是这样的话,当你要添加很多元素时,你不应该尽量避免使用数组吗?您应该改用 iStringMap 吗?如果是这样,如果您需要两个以上的维度并且需要添加大量元素添加,会发生什么情况。您只是受到性能影响还是应该使用其他东西?

【问题讨论】:

【参考方案1】:

查看通用List<T> 作为数组的替代品。它们支持数组所做的大部分事情,包括根据需要分配初始存储大小。

【讨论】:

如果设计可重用或类似框架的组件,请考虑使用基类或接口 ICollection 或 IEnumerable。静态代码分析应该告诉您这一点并链接到更好的信息(它用于继承等)。【参考方案2】:

这真的取决于你所说的“添加”是什么意思。

如果你的意思是:

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

那么,不,这不会创建新数组,实际上是在 .NET 中更改任何类型 IList 的最快方法。

但是,如果您使用的是 ArrayList、List、Collection 等,那么调用“Add”方法可能会创建一个新数组——但他们对此很聪明,他们不要只调整 1 个元素的大小,它们会以几何方式增长,所以如果你只是偶尔添加很多值,它必须分配一个新数组。即使这样,如果您知道要添加多少元素,您也可以使用“Capacity”属性来强制其增长 (list.Capacity += numberOfAddedElements)

【讨论】:

capacity 值的好调用【参考方案3】:

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

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

【讨论】:

【参考方案4】:

如果您要大量添加/删除元素,只需使用列表即可。如果它是多维的,您总是可以使用 List> 或其他东西。

另一方面,如果您的主要工作是遍历列表,则列表的效率低于数组,因为数组都在 CPU 缓存中的一个位置,其中列表中的对象散落在各处。

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

1) 将其生成为 List(或 List of Lists),然后使用 ToArray() 将其转换为高效的数组结构。

2) 分配比您需要的更大的数组,然后将对象放入预先分配的单元格中。如果您最终需要比预分配更多的元素,您可以在数组填满时重新分配,每次将大小加倍。这给了 O(log n) 调整大小的性能,而不是 O(n),就像使用 reallocate-once-per-add 数组一样。请注意,这几乎就是 StringBuilder 的工作方式,为您提供了一种更快的方式来不断地追加到字符串。

【讨论】:

在.NET中,列表实际上是一个ArrayList,它使用一个Array作为后备存储。调用 ToArray 只返回存储值的数组的副本。换句话说,List 是一个可调整大小数组的包装器。 列表不是到处都是。如果您出于某种原因对值类型进行装箱,那么可以肯定,元素会分散开来,但是 List 在内部使用数组,因此在这方面,情况不会更糟。 我必须投反对票,因为它是错误的。得知所有 .NET 集合都是基于数组的,您可能会感到惊讶。我相信原因是为了参考的局部性和减少 GC 开销。 如果你的数组是由对象组成的,它们仍然会分散在各处。该数组将只是一个引用数组。如果您希望数据在数组中,则需要使用结构。 @Mike LinkedList 不是基于数组,但你是对的,这是一个例外。【参考方案5】:

何时放弃使用数组

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

    其次,当您为绝对性能关键性编码时 - 大约 95% 的情况。 Arrays perform better marginally,especially in iteration。它几乎总是无关紧要。

    当你params 关键字的争论所强迫时 - 我只是希望 params 接受任何 IEnumerable&lt;T&gt; 甚至更好语言构造自身来表示一个序列(而不是一个框架类型)。

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

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

    避免使用数组 imo 的最大原因是概念性的。数组更接近实现,远离抽象。数组传达了更多的如何完成,而不是完成了什么,这与高级语言的精神背道而驰。这并不奇怪,考虑到数组更接近金属,它们直接来自一种特殊类型(尽管内部数组是一个类)。不是教学法,但数组确实可以转换为非常罕见的语义含义。最有用和最常见的语义是具有任何条目的集合、具有不同项的集合、键值映射等,以及可添加、只读、不可变、尊重顺序的变体的任意组合。想一想,您可能想要一个可添加的集合,或者带有预定义项目且无需进一步修改的只读集合,但是您的逻辑多久看起来像“我想要一个动态可添加的集合,但只有固定数量的集合,它们也应该是可修改的“?我会说非常罕见。

    Array 是在泛型之前的时代设计的,它通过大量运行时黑客来模仿泛型,它会在这里和那里展示它的古怪之处。我发现的一些问题:

      Broken covariance.

      string[] strings = ...
      object[] objects = strings;
      objects[0] = 1; //compiles, but gives a runtime exception.
      

      Arrays can give you reference to a struct!。这与其他任何地方都不一样。一个样本:

      struct Value  public int mutable; 
      
      var array = new[]  new Value() ;  
      array[0].mutable = 1; //<-- compiles !
      //a List<Value>[0].mutable = 1; doesnt compile since editing a copy makes no sense
      print array[0].mutable // 1, expected or unexpected? confusing surely
      

      Run time implemented methods like ICollection&lt;T&gt;.Contains can be different for structs and classes。这没什么大不了的,但是如果您忘记正确覆盖 non generic Equals 以让引用类型期望泛型集合寻找 generic Equals,您将得到错误结果。

      public class Class : IEquatable<Class>
      
          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<Struct>
      
          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"
      

      T[] 上的 Length 属性和 [] 索引器似乎是您可以通过反射访问的常规属性(这应该涉及一些魔法),但是当涉及到表达式树时,您必须吐出确切的编译器执行的代码相同。有 ArrayLengthArrayIndex 方法可以分别执行此操作。一个这样的question here。另一个例子:

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

如何放弃使用数组

最常用的替代品是List&lt;T&gt;,它具有更简洁的 API。但它是一个动态增长的结构,这意味着您可以在末尾添加List&lt;T&gt; 或在任何位置插入任何容量。无法替代数组的确切行为,但人们大多将数组用作只读集合,您无法在其末尾添加任何内容。替代品是ReadOnlyCollection&lt;T&gt;。我携带这个扩展方法:

public ReadOnlyCollection<T> ToReadOnlyCollection<T>(IEnumerable<T> source)

    return source.ToList().AsReadOnly();

【讨论】:

给定Point[] myPts;,声明myPts[3].X+=3; 将影响myPts[3]X 属性,而不会影响宇宙中任何其他PointX 属性。在我看来是一件的事情。在您的示例中,为什么 array[0].mutable 不应该为 1?想假装宇宙中的一切都是类对象的人可能不喜欢结构,但是“简单”可变结构类型的数组的含义比可变类类型数组的含义不言而喻。 supercat,我真的对它是好事还是坏事没有任何意见(我个人喜欢引用语义,因为这是我一直在处理的,并且从不需要编写自己的结构) .但是我认为数组的索引器实现不好,因为它与结构在其他地方的行为方式不一致。 array[0].mutable 不应该是 1,因为我被告知并且我体验到我每次都会获得一个新的价值。这就是所有其他 [] 索引器、方法、变量赋值等所做的。 其他索引属性的行为不像数组槽的事实直接类似于其他非索引属性的行为不像字段的事实。从 .NET 2.0 开始,如果 TProp 类型的属性有一个标准属性模板 AccessValue&lt;TExtra&gt;(ActionByRef&lt;TProp, TExtra&gt; act, ref TExtra extraParam); 如果这样的东西包含在 IList&lt;T&gt;、@987654358 中,则可以合理有效地使属性表现得像变量@ 可能已经实现了类似: T IndexedAccessValue&lt;TExtra&gt;(int index, ActionByRef&lt;TProp, TExtra&gt; act, ref TExtra extraParam) extra) act(ref _arr[index], ref extraParam); 不幸的是,由于 List&lt;T&gt; 的后备数组是私有的,因此无法创建可用作 List&lt;T&gt; 的类型,但也允许其项目用作 @987654362 @参数以这种方式。【参考方案6】:

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

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

【讨论】:

【参考方案7】:

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

当您的基准测试表明调整数组大小严重拖慢了您的应用程序时(请记住 - 过早优化是万恶之源),您可以评估编写一个调整大小调整行为的自定义数组类。

【讨论】:

【参考方案8】:

一般来说,如果您必须拥有最好的索引查找性能,最好先构建一个 List,然后将其转换为一个数组,这样一开始会付出一点点代价,但以后要避免。如果问题是您将不断添加新数据和删除旧数据,那么您可能希望使用 ArrayList 或 List 方便,但请记住它们只是特殊情况下的数组。当它们“增长”时,它们会分配一个全新的数组并将所有内容复制到其中,这非常慢。

ArrayList 只是一个在需要时增长的数组。 Add 是摊销 O(1),只是要小心确保调整大小不会在错误的时间发生。 插入是 O(n) 必须将右侧的所有项目移过来。 删除是 O(n) 必须将右边的所有项目都移过去。

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

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

在这种特殊情况下,我会说如果你能想出一个好的键值Dictionary 将是你最好的选择。它具有接近 O(1) 的插入和删除。但是,即使使用 Dictionary,您也必须小心不要让它调整其内部数组的大小(O(n) 操作)。最好通过在构造函数中指定大于您预期使用的初始容量来给它们很大的空间。

-瑞克

【讨论】:

【参考方案9】:

一个标准数组应该定义一个长度,它在一个连续的块中保留它需要的所有内存。向数组中添加一项会将其放入已保留的内存块中。

【讨论】:

【参考方案10】:

数组非常适合少量写入和多次读取,尤其是那些具有迭代性质的 - 对于其他任何事情,请使用许多其他数据结构中的一种。

【讨论】:

【参考方案11】:

您是正确的,数组非常适合查找。但是,修改数组大小的成本很高。

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

或者你可以只使用一个链表。然后,但是查找速度很慢...

【讨论】:

不要在 .Net 2.0 及更高版本中使用 ArrayList。对您要存储的类型使用通用 List【参考方案12】:

如果我认为我要在集合的整个生命周期内向集合中添加很多项目,那么我将使用列表。如果我确定声明集合时集合的大小,那么我将使用数组。

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

【讨论】:

您可能需要重新考虑从属性中返回内部列表的数组副本:***.com/questions/35007/…【参考方案13】:

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

【讨论】:

【参考方案14】:

您可以做的最好的事情是尽可能多地分配您需要的内存。这将防止.NET 不得不进行额外的调用来获取堆上的内存。如果做不到这一点,那么以 5 块或任何对您的应用程序有意义的数量进行分配是有意义的。

这条规则真的可以应用于任何事情。

【讨论】:

以上是关于如何以及何时放弃在 C# 中使用数组?的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中何时使用抽象类以及何时使用接口 [重复]

C# 使用流

我应该如何以及何时将倾斜指针与 cuda API 一起使用?

有人可以解释一下如何以及何时应该在 oracle 中使用 syscursor 吗? [关闭]

何时以及如何使用休眠二级缓存?

何时以及为啥需要supportedRuntime 元素和sku 属性?