Array类是所有一维和多维数组的隐式基类,同时也是实现标准集合接口的最基本的类型。Array类实现了类型统一,因此它为所有数组提供了一组通用的方法,不论这些数组元素的类型,这些通用的方法均适用。

正因为数组如此重要,所以C#为声明数组和初始化数组提供了明确的语法。在使用C#语法声明一个数组时,CLR隐式地构建Array类--合成一个伪类型以匹配数组的维数和数组元素的类型。而且这个伪类型实现了generic集合接口,比如IList<string>接口。

CLR在创建数组类型实例时会做特殊处理--在内存中为数组分配连续的空间。这就使得索引数组非常高效,但这却阻止了对数组的修改或调正数组大小。

Array类实现了IList<T>接口和IList接口。Array类显示地实现了IList<T>接口,这是为了保证接口的完整性。但是在固定长度集合比如数组上调用IList<T>接口的Add或Remove方法时,会抛出异常(因为数组实例一旦声明之后,就不能更改数组的长度)。Array类提供了一个静态的Resize方法,使用这个方法创建一个新的数组实例,然后复制当前数组的元素到新的实例。此外,在程序中,当在任何地方引用一个数组都会执行最原始版本的数组实例。因此如果希望集合大小可以调整,那么你最好使用List<T>类。

数组元素可以是值类型也可以是引用类型。值类型数组元素直接存在数组中,比如包含3个整数的数组会占用24个字节的连续内存。而引用类型的数组元素,占用的空间和一个引用占用的空间一样(32位环境中4个字节,64为环境中8个字节)。我们先来看下面的代码:

  1. int[] numbers =new int[3];
  2. numbers[0] =1;
  3. numbers[1] = 7;
  4.  
  5. StringBuilder[] builders = new StringBuilder[5];
  6. builders[0] = new StringBuilder("Builder1");
  7. builders[1] = new StringBuilder("Builder2");
  8. builders[2] = new StringBuilder("Builder3");

对应的内存分配变化如下面几张图所示:

执行完int[] numbers=new int[3]之后,在内存中分配了8×3=24个字节,每个字节都为0。

执行完numbers[0]=1之后,数组声明后的第一个8字节变为00 00 00 01。

同样地,执行完numbers[1]=7之后,第二段8个字节变为00 00 00 07。

对应引用类型的数组,我们用一张图来说明内存分配:

看起来分复杂,其实内存分配示意图如下

通过Clone方法可以克隆一个数组,比如arrayB = arrayA.Clone()。但是,克隆数组执行浅拷贝,也就是说数组自己包含的那部分内容才会被克隆。简单说,如果数组包含的是值类型对象,那么克隆了这些对象的值。而数组的子元素是引用类型,那么仅仅克隆引用类型的地址。

  1. StringBuilder[] builders2 = builders;
  2. ShtringBuilder[] shallowClone = (StringBuilder[])builders.Clone();

与之对应的内存分配示意图:

如果需要执行深拷贝,即克隆引用类型的子对象;那么你需要遍历数组并手动的克隆每个数组元素。深克隆规则也适用于.NET其他集合类型。

尽管数组在设计时,主要使用32位的索引,它也在一定程度上支持64位索引,这需要使用那些既接收Int32又接收Int64类型参数的方法。这些重载方法在实际中并没有意义,因为CLR不允许任何对象--包括数组在内--的大小超过2G(无论32位的系统还是64位的系统)

构造数组和索引数组

创建数组和索引数组的最简单方式就是通过C#语言的构建器

  1. Int[] myArray={1,2,3};
  2. int first=myArray[0];
  3. int last = myArray[myArray.Length-1];

或者,你可以通过调用Array.CreateInstance方法动态地创建一个数组实例。你可以通过这种方式指定数组元素的类型和数组的维度。而GetValue和SetValue方法允许你访问动态创建的数组实例的元素。

  1. Array a = Arrat.CreateInstance(typeof(string), 2);
  2. a.SetValue('hi", 0);
  3. a.SetValue("there",1);
  4. string s = (string)a.getValue(0);
  5.  
  6. string[] cSharpArray = (string[])a;
  7. string s2 = cSharpArray[0];

动态创建的零索引的数组可以转换为一个匹配或兼容的C#数组。比如,如果Apple是Fruit的子类,那么Apple[]可以转换成Fruit[]。这也是为什么object[]没有作为统一的数组类型,而是使用Array类;答案在于object[]不仅与多维数组不兼容,而且还与值类型数组不兼容。因此我们使用Array类作为统一的数组类型。

GetValue和SetValue对编译器生成的数组也起作用,若想编写一个方法处理任何类型的数组和任意维度的数组,那么这两个方法非常有用。对于多维数组,这两个方法可以把一个数组当作索引参数。

  1. public object GetValue(params int[] indices)
  2. public void SetValue(object value, params int[] indices)

下面的方法在屏幕打印任意数组的第一个元素,无论数组的维度

  1. void WriteFirstValue (Array a)
  2. {
  3. Console.Write (a.Rank + "-dimensional; ");
  4.  
  5. int[] indexers = new int[a.Rank];
  6. Console.WriteLine ("First value is " + a.GetValue (indexers));
  7. }
  8. void Demo()
  9. {
  10. int[] oneD = { 1, 2, 3 };
  11. int[,] twoD = { {5,6}, {8,9} };
  12. WriteFirstValue (oneD); // 1-dimensional; first value is 1
  13. WriteFirstValue (twoD); // 2-dimensional; first value is 5
  14. }

在使用SetValue方法时,如果元素与数组类型不兼容,那么会抛出异常。

无论采取哪种方式实例化一个数组之后,那么数组元素自动初始化了。对于引用类型元素的数组而言,初始化数组元素就是把null值赋给这些数组元素;而对于值类型数组元素,那么会把值类型的默认值赋给数组元素。此外,调用Array类的Clear方法也可以完成同样的功能。Clear方法不会影响数组大小。这和常见的Clear方法(比如ICollection<T>.Clear方法)不一样,常见的Clear方法会清除集合的所有元素。

遍历数组

通过foreach语句,可以非常方便地遍历数组:

  1. int[] myArray = { 1, 2, 3};
  2. foreach (int val in myArray)
  3. Console.WriteLine (val);

你还可以使用Array.ForEach方法来遍历数组

  1. public static void ForEach<T> (T[] array, Action<T> action);

该方法使用Action代理,此代理方法的签名是(接收一个参数,不返回任何值):

  1. public delegate void Action<T> (T obj);

下面的代码显示了如何使用ForEach方法

  1. Array.ForEach (new[] { 1, 2, 3 }, Console.WriteLine);

你可能会很好奇Array.ForEach是如何执行的,它就是这么执行的

  1. public static void ForEach<T>(T[] array, Action<T> action) {
  2. if( array == null) {
  3. throw new ArgumentNullException("array");
  4. }
  5.  
  6. if( action == null) {
  7. throw new ArgumentNullException("action");
  8. }
  9. Contract.EndContractBlock();
  10.  
  11. for(int i = 0 ; i < array.Length; i++) {
  12. action(array[i]);
  13. }
  14. }

在内部执行for循环,并调用Action代理。在上面的实例中,我们调用Console.WriteLine方法,所以可以在屏幕上输出1,2,3。

获取数组的长度和维度

Array提供了下列方法或属性以获取数组的长度和数组的维度:

  1. public int GetLength (int dimension);
  2. public long GetLongLength (int dimension);
  3.  
  4. public int Length { get; }
  5. public long LongLength { get; }
  6.  
  7. public int GetLowerBound (int dimension);
  8. public int GetUpperBound (int dimension);
  9.  
  10. public int Rank { get; }

GetLength和GetLongLength返回指定维度的长度(0表示一维数组),Length和LongLength返回数组中所有元素的总数(包含了所有维度)。

GetLowerBound和GetUpperBound对于多维数组非常有用。GetUpperBound返回的结果等于指定维度的GetLowerBound+指定维度的GetLength

搜索数组

Array类对外提供了一系列方法,以在一个维度中查找元素。比如:

  • BinarySearch方法:在一个排序后的数组中快速找到指定元素;

  • IndexOf/LastIndex方法:在未排序的数组中搜索指定元素;
  • Find/FindLast/FindIndex/FindLastIndex/FindAll/Exists/TrueForAll方法:根据指定的Predicated<T>(代理)在未排序的数组中搜索一个或多个元素。

如果没有找到指定的值,数组的这些搜索方法不会抛出异常。相反,搜索方法返回-1(假定数组的索引都是以0开始),或者返回generic类型的默认值(int返回0,string返回null)。

二进制搜索速度很快,但是它仅仅适用于排序后的数组,而且数组的元素是根据大小排序,而不是根据相等性排序。正因为如此,所以二进制搜索方法可以接收IComparer或IComparer<T>对象以对元素进行排序。传入的IComparer或IComparer<T>对象必须和当前数组所使用的排序比较器一致。如果没有提供比较器参数,那么数组会使用默认的排序算法。

IndexOf和LastIndexOf方法对数组进行简单的遍历,然后根据指定的值返回第一个(或最后一个)元素的位置。

以断定为基础(predicate-based)的搜索方法接受一个方法代理或lamdba表达式判断元素是否满足“匹配”。一个断定(predicate)是一个简单的代理,该代理接收一个对象并返回bool值:

  1. public delegate bool Precicate<T>(T object);

下面的例子中,我们搜索字符数组中包含字母A的字符:

  1. static void Main(string[] args)
  2. {
  3. string[] names = { "Rodney", "Jack", "Jill" };
  4. string match = Array.Find(names, ContainsA);
  5. Console.WriteLine(match);
  6.  
  7. Console.ReadLine();
  8. }
  9.  
  10. static bool ContainsA(string name)
  11. {
  12. return name.Contains("a");
  13. }

上面的代码可以简化为:

  1. static void Main(string[] args)
  2. {
  3. string[] names = { "Rodney", "Jack", "Jill" };
  4. string match = Array.Find(names, delegate(string name) { return name.Contains("a"); });
  5. Console.WriteLine(match);
  6.  
  7. Console.ReadLine();
  8. }

如果使用lamdba表达式,那么代码还可以更简洁:

  1. static void Main(string[] args)
  2. {
  3. string[] names = { "Rodney", "Jack", "Jill" };
  4. string match = Array.Find(names, name=>name.Contains("a"));
  5. Console.WriteLine(match);
  6.  
  7. Console.ReadLine();
  8. }

FindAll方法则从数组中返回满足断言(predicate)的所有元素。实际上,该方法等同于Enumerable.Where方法,只不过数组的FindAll是从数组中返回匹配的元素,而Where方法从IEnumerable<T>中返回。

如果数组成员满足指定的断言(predicate),那么Exists方法返回True,该方法等同于Enumerable.Any方法。

所以数组的所有成员都满足指定的断言(predicate),那么TrueForAll方法返回True,该方法等同于Enumerable.All方法。

对数组排序

数组有下列自带的排序方法:

  1. public static void Sort<T>(T[] array);
  2. public static void Sort(Array array);
  3. public static void Sort(TKey, TValue)(TKey[] keys, TValue[] items);
  4. public static void Sort(Array keys[], Array items);

上面的方法都有重载的版本,重载方法接受下面这些参数:

  • int index,从指定索引位置开始排序

  • int length,从指定索引位置开始,需要排序的元素的个数
  • ICompare<T> comparer,用于排序决策的对象
  • Comparison<T> comparison,用于排序决策的代理

下面的代码演示了如何实现一个简单的排序:

  1. static void Main(string[] args)
  2. {
  3. int[] numbers = { 3,2,1};
  4. Array.Sort(numbers);
  5. foreach (int number in numbers)
  6. Console.WriteLine(number);
  7.  
  8. Console.ReadLine();
  9. }

Sort方法还可以接收两个两个数组类型的参数,然后基于第一个数组的排序结果,对每个数组的元素进行排序。下面的例子中,数字数组和字符数组都按照数字数组的顺序进行排序。

  1. static void Main(string[] args)
  2. {
  3. int[] numbers = { 3,2,1};
  4. string[] names = { "C", "B", "E" };
  5. Array.Sort(numbers, names);
  6. foreach (int number in numbers)
  7. Console.WriteLine(number); // 1, 2,3
  8.  
  9. foreach (string name in names)
  10. Console.WriteLine(name); // E, B, C
  11.  
  12. Console.ReadLine();
  13. }

Array.Sort方法要求数组实现了IComparer接口。这就是说C#的大多数类型都可以排序。如果数组元素不能进行比较,或你希望重载默认的排序,那么你需要在调用Sort方法时,需提供自定义的Comparison。所以自定义排序算法有下面两种实现方式:

1)通过一个帮助对象实现IComparer或IComparer<T>接口

  1. public static void Sort(Array array, IComparer comparer)
  2. public static void Sort<T>(T[] array, System.Collections.Generic.IComparer<T> comparer)

2)通过Comparison接口

  1. public static void Sort<T>(T[] array, Comparison<T> comparison)

Comparison代理遵循IComparer<T>.CompareTo语法:

  1. public delegate int Comparison<T> (T x, T y);

如果x的位置在y之前,那么返回-1;如果x在y之后,返回1,如果位置相同,那么返回0。

我们来看一下Array的Sort<T>(T[]array, Comparison<T> comparison)方法的源代码:

  1. public static void Sort<T>(T[] array, Comparison<T> comparison) {
  2. ......
  3.  
  4. IComparer<T> comparer = new FunctorComparer<T>(comparison);
  5. Array.Sort(array, comparer);
  6. }

由此,可内部,Comparison<T>转换成IComparer<T>,因此在实际中,需要实现自定义排序时,如果需要考虑性能,那么推荐使用第一种方式。此外,我们分析Sort<T>(T[] array, System.Collections.Generic.IComparer<T> comparer)的源代码,

  1. public static void Sort<T>(T[] array, int index, int length, System.Collections.Generic.IComparer<T> comparer)
  2. {
  3. ...
  4.  
  5. if (length > 1)
  6. {
  7. if (comparer == null || comparer == Comparer<T>.Default)
  8. {
  9. if (TrySZSort(array, null, index, index + length - 1))
  10. {
  11. return;
  12. }
  13. }
  14.  
  15. ArraySortHelper<T>.Default.Sort(array, index, length, comparer);
  16. }
  17. }

我们可以看到,首先尝试调用调用非托管代码的Sort方法,如果成功排序,直接返回。否则调用非托管代码(C#的ArraySortHelper)的Sort方法进行排序:

  1. public void Sort(T[] keys, int index, int length, IComparer<T> comparer)
  2. {

  3. try
  4. {
  5. if (comparer == null)
  6. {
  7. comparer = Comparer<T>.Default;
  8. }
  9.  
  10. if (BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
  11. {
  12. IntrospectiveSort(keys, index, length, comparer);
  13. }
  14. else
  15. {
  16. DepthLimitedQuickSort(keys, index, length + index - 1, comparer, IntrospectiveSortUtilities.QuickSortDepthThreshold);
  17. }
  18. }
  19. catch (IndexOutOfRangeException)
  20. {
  21. IntrospectiveSortUtilities.ThrowOrIgnoreBadComparer(comparer);
  22. }
  23. catch (Exception e)
  24. {
  25. throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"), e);
  26. }
  27. }

如果有兴趣,可以继续分析IntrospectiveSort方法和DepthLimitedQuickSort,不过MSDN已经给出了总结,

意识是说,排序算法有三种:

  • 如果分区大小小于16,那么使用插入排序算法

  • 如果分区的大小超过2*LogN,N是数组的范围,那么使用堆排序
  • 其他情况,则使用快排

反转数组的元素

使用下面的方法,可以反转数组的所有元素或部分元素

  1. public static void Reverse (Array array);
  2. public static void Reverse (Array array, int index, int length);

如果你在乎性能,那么请不要直接调用Array的Reverse方法,而是应该创建一个自定义的RerverseComparer。比如下面的例子中,调用Array.Reverse和CustomReverse在我的电脑上两者的性能高差距为20%左右

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. int seeds = 100000;
  6.  
  7. Staff[] staffs = new Staff[seeds];
  8. Random r = new Random();
  9. for (int i = 0; i < seeds; i++)
  10. staffs[i] = new Staff { StaffNo = r.Next(1, seeds).ToString() };
  11.  
  12. ArrayReverse(staffs);
  13. CustomReverse(staffs);
  14.  
  15. Console.ReadLine();
  16. }
  17.  
  18. static void ArrayReverse(Staff[] staffs)
  19. {
  20. DateTime t1 = DateTime.Now;
  21. Array.Sort(staffs);
  22. Array.Reverse(staffs);
  23. DateTime t2 = DateTime.Now;
  24. Console.WriteLine("Array Reverse: " + (t2 - t1).Milliseconds + "ms");
  25. }
  26.  
  27. static void CustomReverse(Staff[] staffs)
  28. {
  29. DateTime t1 = DateTime.Now;
  30. Array.Sort(staffs, new StaffComparer());
  31. DateTime t2 = DateTime.Now;
  32. Console.WriteLine("Custom Reverse: " + (t2 - t1).Milliseconds + "ms");
  33. }
  34.  
  35. internal class Staff : IComparable
  36. {
  37. public string StaffNo { get; set; }
  38. public string Name { get; set; }
  39.  
  40. public int CompareTo(object obj)
  41. {
  42. Staff x = obj as Staff;
  43.  
  44. return this.StaffNo.CompareTo(x.StaffNo);
  45. }
  46. }
  47.  
  48. internal class StaffComparer : IComparer<Staff>
  49. {
  50. public int Compare(Staff x, Staff y)
  51. {
  52. return y.StaffNo.CompareTo(x.StaffNo);
  53. }
  54. }
  55. }

执行结果:

复制数组

Array提供了四个方法以实现浅拷贝:Clone,CopyTo,Copy和ConstrainedCopy。前两个方法是实例方法,后面两个是静态方法。

Clone方法返回一个全新的(浅拷贝)数组 。CopyTo和Copy方法复制数组的连续子集。复制一个多维矩形数组需要你的多维索引映射到一个线性索引。比如,一个3×3的数组position,那么postion[1,1]对应的线性索引为1*3+1=4。原数组和目标数组的范围可以交换,不会带来任何问题。

ConstrainedCopy执行原子操作,如果所要求的元素不能全部成功地复制,那么操作回滚。

Array还提供AsReadOnly方法,它返回一个包装器,以防止数组元素的值被更改。

最后,Clone方法是由外部的非托管代码实现

  1. protected extern Object MemberwiseClone()

同样地,Copy,CopyTo, ConstraintedCopy也都是调用外部实现

  1. internal static extern void Copy(Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length, bool reliable);

转换数组和缩减数组大小

Array.ConvertAll创建并返回一个类型为TOutput的新数组,调用Converter代理以复制元素到新的数组中。Converter的定义如下:

  1. public delegate TOutput Converter<TInput,TOutput>(TInput input)

下面的代码展示了如果把一个浮点数数组转换成int数组

  1. float[] reals = { 1.3f, 1.5f, 1.8f };
  2.  
  3. int[] wholes = Array.ConvertAll(reals, f => Convert.ToInt32(f));
  4.  
  5. foreach (int a in wholes)
  6. Console.WriteLine(a); //->1,2,2

Resize方法创建一个新数组,然后复制元素到新数组,然后返回新数组;原数组不发生改变。

C#集合--数组的更多相关文章

  1. ruby pluck用法,可以快速从数据库获取 对象的 指定字段的集合数组

    可以快速从数据库获取 对象的 指定字段的集合数组 比如有一个users表,要等到user的id数组: select id from users where age > 20; 要实现在如上sql ...

  2. 集合 数组 定义 转换 遍历 Arrays API MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  3. jquery遍历集合&数组&标签

      jquery遍历集合&数组的两种方式 CreateTime--2017年4月24日08:31:49Author:Marydon 方法一: $(function(){ $("inp ...

  4. Delphi基本类型--枚举 子界 集合 数组

    [plain] view plain copy <strong>根据枚举定义集合 </strong> TMyColor = (mcBlue, mcRed); TMyColorS ...

  5. 【Scala篇】--Scala中集合数组,list,set,map,元祖

    一.前述 Scala在常用的集合的类别有数组,List,Set,Map,元祖. 二.具体实现 数组   1.创建数组 new Array[Int](10) 赋值:arr(0) = xxx Array[ ...

  6. SpringMVC,SpringBoot使用ajax传递对象集合/数组到后台

    假设有一个bean名叫TestPOJO. 1.使用ajax从前台传递一个对象数组/集合到后台. 前台ajax写法: var testPOJO=new Array(); //这里组装testPOJO数组 ...

  7. Ruby 集合数组常用遍历方法

    迭代器简介 先简单介绍一下迭代器. 1.一个Ruby迭代器就是一个简单的能接收代码块的方法(比如each这个方法就是一个迭代器).特征:如果一个方法里包含了yield调用,那这个方法肯定是迭代器: 2 ...

  8. SpringMVC方法传递集合数组

    背景:实体集合作为参数   数据准备: 1.实体类 class A {private int id; private String name; } 2.集合json字符串 [{"id&quo ...

  9. 集合数组与String的互转

    1.集合转成数组: 转之前集合里面存的什么类型的数据,就new什么类(特别:存的是基本数据的封装类,就要new他的封装类) 例如: 1.1集合: ArrayList<Character> ...

随机推荐

  1. C++语法之-------strcpy,memcpy,memset

    1.strcpy 原型:extern char *strcpy(char *dest,char *src); 用法:#i nclude 功能:把src所指由NULL结束的字符串复制到dest所指的数组 ...

  2. Data层相关问题 & JS循环取值

    第一次写博客,里面是自己工作中碰到的问题及总结的知识点,便于自己以后回顾,技术大牛们请直接忽略这篇文章,也希望能帮助到想我这样的小白! Data层相关问题总结: 1. 代码管理用的是 VSS 2005 ...

  3. php乱码

    1:php 编码是utf-8 但是接口需要是gbk 这么办?? $message = "你的微点管理地址为:".$shortUrl." [请保存信息]"; // ...

  4. [ASE][Daily Scrum]11.24

    今天开会总结了一下第一周的进度,讨论了无限地图的访存方法,做了简单的人员调整, Client的包接收分析与服务器通信这块基本上完成了, 之后Jiafan Zhu会开始和Junbei以及Songtao一 ...

  5. iOS URL 编码

    一.iOS 中的NSURL编码 iOS 中,NSURL 的基本样式是 scheme://username:password@host:port/path?query#fragment RFC 1738 ...

  6. 蛙蛙推荐:如何实时监控MySql状态

    大多网站的性能瓶颈都会出在数据库上,所以想把Mysql监控起来,就搜索了下相关资料. 后来和同事讨论了下cacti和nagios有些老套和过时,graphite比较时尚,然后就搜了下相关的资料,最后搞 ...

  7. [安卓] 5、SeekBar拖动条

    越来越发现这些控件用法大同小异了,这里注意几个函数:seekBar.setSecondaryProgress(0);设置初始进度为0,总共为0~99,对其监听用setOnSeekBarChangeLi ...

  8. 阿里云服务器PPTP VPN安装记录

    # sudo apt-get install pptpd   http://blog.kunyu.li/digitalocean-ubuntu-vps-vpn.html     iptables管理 ...

  9. SVG的路径动画效果

    使用SVG animateMotion实现的一个动画路径效果,相关代码如下. 在线调试唯一地址:http://www.gbtags.com/gb/debug/c88f4099-5056-4ad7-af ...

  10. BOM (Browser Object Model) 浏览器对象模型

    l对象的角色,因此所有在全局作用域中声明的变量/函数都会变成window对象的属性和方法; // PS:尝试访问未声明的变量会抛出错误,但是通过查询window对象,可以知道某个可能未声明的对象是否存 ...