关于.net:如何以及何时放弃在C#中使用数组?

关于.net:如何以及何时放弃在C#中使用数组?

How and when to abandon the use of arrays in C#?

我总是被告知,将元素添加到数组的过程如下:

An empty copy of the array+1element is
created and then the data from the
original array is copied into it then
the new data for the new element is
then loaded

如果是这样,那么由于内存和CPU使用率的原因,禁止在需要大量元素活动的情况下使用数组,对吗?

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


将通用List< T >视为数组的替代品。它们支持阵列执行的大多数相同操作,包括根据需要分配初始存储大小。


这真的取决于您所说的"添加"。

如果你的意思是:

1
2
3
4
5
6
T[] array;
int i;
T value;
...
if (i >= 0 && i <= array.Length)
    array[i] = value;

然后,不,这不会创建新的数组,并且实际上是更改.NET中任何IList的最快方法。

但是,如果您使用的是ArrayList,List,Collection等,则调用" Add"方法可能会创建一个新数组-但他们很聪明,它们不仅会按1个元素调整大小,以几何方式增长,因此,如果您不时地一次添加大量值,则必须分配一个新数组。即使这样,如果您知道要添加的元素数(list.Capacity += numberOfAddedElements),也可以使用" Capacity"属性强制其预先增长。


如果要大量添加/删除元素,只需使用列表即可。如果是多维的,则始终可以使用List >之类的东西。

另一方面,如果遍历列表主要是在做列表,则效率不如数组,因为数组全部位于CPU缓存中的一个位置,列表中的对象分散在整个位置。

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

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

2)分配数组,使其大于所需的大小,然后将对象放入预分配的单元格中。如果最终需要的元素比预先分配的更多,则可以在数组填满时重新分配数组,每次增加一倍。这样就可以为O(log n)调整大小,而不是像重新分配一次添加数组那样的O(n)。请注意,这几乎就是StringBuilder的工作方式,从而为您提供了一种更快速地连续追加到字符串的方法。


通常,我更喜欢避免使用数组。只需使用List < T >。它在内部使用动态大小的数组,并且对于大多数用法而言足够快。如果使用多维数组,则必须使用List >>。就内存而言,它并没有那么糟糕,并且添加项目要简单得多。

如果您需要0.1%的使用速度,那么在尝试对其进行优化之前,请确保列表访问确实是问题所在。


When to abandon the use of arrays

  • 首先,当数组的语义与您的意图不符时-需要动态增长的集合吗?不允许重复的套装?必须保持不变的集合吗?在所有情况下都避免使用数组。那是99%的情况。仅说明显而易见的基本要点。

  • 其次,当您不为绝对的性能关键性进行编码时-大约是95%的情况。数组的性能略佳,尤其是在迭代中。几乎总是没有关系。

  • 当您不被params关键字的参数强制时-我只希望params接受任何IEnumerable< T >甚至更好的语言构造本身来表示序列(而不是框架类型)。

  • 当您不编写旧版代码或不处理互操作时

  • 简而言之,实际上很少需要数组非常罕见。我还要补充一下为什么可以避免这种情况?

  • 避免数组imo的最大原因是概念上的。数组离实现更近,而离抽象更远。与违反高级语言精神的做法相比,数组所传达的作用更多。考虑到数组更接近金属,它们是特殊类型的(尽管内部数组是一类),这并不奇怪。并不是教学论,而是数组确实确实转换成非常很少需要的语义。最有用和最常见的语义是具有任何条目的集合,具有不同项目的集合,键值映射等的集合,这些集合具有可加,只读,不可变,遵守顺序的变体的任意组合。考虑一下,您可能需要一个可添加的集合,或者是具有预定义项的只读集合,而无需进行进一步修改,但是您的逻辑看起来像"我想要一个可动态添加的集合,但是只有固定数量的集合,它们也应该可以修改""?我会说非常罕见。

  • Array是在前泛型时代设计的,它通过大量的运行时hack模仿通用性,并且会在这里和那里展示出它的怪异之处。我发现的一些收获:

  • 破碎的协方差。

    1
    2
    3
    string[] strings = ...
    object[] objects = strings;
    objects[0] = 1; //compiles, but gives a runtime exception.
  • 数组可以为您提供对struct!的引用。这不同于其他任何地方。一个样品:

    1
    2
    3
    4
    5
    6
    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
  • 对于类型和结构,运行时实现的方法(如ICollection< T >.Contains)可以不同。没什么大不了的,但是如果您忘记为期望泛型集合查找泛型Equals的引用类型正确覆盖非泛型Equals,则会得到错误的结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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方法可以分别执行此操作。这里有一个这样的问题。另一个例子:

    1
    2
    3
    4
    5
    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;
  • How to abandon the use of arrays

    最常用的替代方法是List< T >,它具有更清晰的API。但这是一个动态增长的结构,这意味着您可以在末尾添加List< T >或在任意位置插入任何容量。不能替代数组的确切行为,但是人们通常将数组用作只读集合,在数组中您无法在其末尾添加任何内容。替代为ReadOnlyCollection< T >。我带有以下扩展方法:

    1
    2
    3
    4
    public ReadOnlyCollection< T > ToReadOnlyCollection< T >(IEnumerable< T > source)
    {
        return source.ToList().AsReadOnly();
    }

    通常,如果您必须具有BEST索引查找性能,则最好先构建一个List,然后将其转换为数组,这样一开始要付出一点代价,但要避免以后再使用。如果问题在于您将不断添加新数据并删除旧数据,则为方便起见,可能需要使用ArrayList或List,但请记住,它们只是特殊情况的Arrays。当他们"增长"时,他们分配一个全新的数组并将所有内容复制到其中,这非常慢。

    ArrayList只是一个需要时增长的Array。
    Add是摊销的O(1),请注意确保不会在不好的时候发生调整大小。
    插入为O(n)右边的所有项目都必须移到上方。
    删除为O(n)右边的所有项目都必须移动。

    请记住,列表不是链接列表也很重要。这只是一个类型化的ArrayList。列表文档确实指出在大多数情况下它的性能更好,但是没有说明原因。

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

    在这种情况下,我想说的是,如果您能拿出一个不错的键值,Dictionary将是您的最佳选择。它具有插入和删除接近O(1)的功能。但是,即使使用了Dictionary,也必须注意不要让它调整其内部数组的大小(O(n)操作)。最好通过在构造函数中指定更大的-然后您期望使用的初始容量为它们留出很多空间。

    -里克


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

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


    调整数组大小时,必须分配新的数组,然后复制内容。如果仅修改数组的内容,则仅是内存分配。

    因此,当您不知道数组的大小或大小可能更改时,请勿使用数组。但是,如果您有固定长度的数组,则它们是按索引检索元素的简便方法。


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


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

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


    对于各种阵列类型的效率,此论坛帖子对您可能有用或可能不有用:
    C#数组-多维与词典


    您是对的,数组非常适合查找。然而,对阵列尺寸的修改是昂贵的。

    在修改数组大小的情况下,应使用支持增量大小调整的容器。您可以使用ArrayList来设置初始大小,并且可以连续检查大小与容量的关系,然后将容量增加一大块以限制调整大小的次数。

    或者,您可以只使用链接列表。然后,但是查找起来很慢...


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


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


    您可以做的最好的事情就是尽可能分配所需的内存。这将防止.NET进行额外的调用来获取堆上的内存。否则,有意义的是将五个或任何数量的块分配给您的应用程序。

    这是一条规则,您可以真正应用于任何事物。


    推荐阅读