为了解决大量的同类型元素,于是数组就孕育而生了。数组是具有一定顺序关系的若干对象的集合体,一维数组可以看作是定长的线性表。反之,n为的数组可以看作线性表的推广。从存储结构上来看,数组是一段连续的存储空间。现在我们看看在C#中的数组:
1、普通数组
在C#中普通数组又可以分为一维数组、多维数组和锯齿数组。
<1>一维数组
我们现在先看看一维数组的声明语法:
类型[] 变量名;
知道怎么声明了,现在我们继续看看数组的初始化吧,在C#中有4种初始化的方式:
//n为数组长度,an为数组内部元素 类型[] 数组名 = new 类型[n]; //为数组分配内存,但是没有赋初值(vs会自动为其赋初值为0) 类型[] 数组名 = new 类型[n]{a1, a2, …, an} //初始化,并赋初值 类型[] 数组名 = new 类型[]{a1, a2 ,…, an} //还可以不指定数组长度,编译器会自动统计元素个数 类型[] 数组名 = {a1, a2,…, an} //C风格的初始化并赋值
访问数组时,以”数组名[i]”的方式访问第 i-1个元素。如果不知道数组的长度,可以使用Length属性。
注意:如果数组中的元素类型是引用类型,就必须为每个元素分配内存。在C#中”类型[]”是一个不可分割的整体,”类型[]”可以看成是 数组类型。
<2>多维数组
看完一维数组,现在我们推广到多维数组,声明语法:
类型[,] 数组名; //二维数组 类型[,,] 数组名; //三维数组
相信你也发现了吧,方括号内的逗号数 + 1 就是数组的维数。我们以二维数组为例,来看看多维数组的初始化:
int[,] arr = new int[2,3]{{1,2,3},{4,5,6}};
借这个例子我想说明多维数组和一维数组初始化的区别就是,多维数组初始化时,每一维度都必须使用大括号括起来。其余的和一维数组初始化方法一样。
<3>锯齿数组
在使用多维数组的过程中,我们有时并不需要每一维度都一样,于是我们就引入了锯齿数组。(在C++中的Vector也有类似的功能)。上一幅图说明二维数组与锯齿数组的区别:
现在我们看看他的声明语法:
类型[][] 数组名 = new 类型[n][]; //n为锯齿数组的维度,后一个方括号为空
我们用一个具体实例来说看看他的使用方法:
int[][] Testarray = new int[2][]; Testarray[0] = new int[3]{1,2,3}; //当然也可以先不赋初值,建议都先赋初值 Testarray[1] = new int[4]{1,2,3,4};
这时候有些人可能会有疑问,每一维度的长度不同,那样怎么简单的遍历整个数组呢?这时Length属性就可以发挥它的优势了。我们以上述的为例:
for(int i = 0; i < Testarray.Length; i++){ for(int j = 0; j < Testarray[i].Length; j++){ //TODO: } }
<4>数组作为参数
既然我们将数组看成一个类型,那么它自然也是可以作为参数传递给方法,也可以从方法中返回。C#数组还支持协变,但是数组协变只能用于引用类型,不能用于值类型。
<5>数组段ArraySegment<T>
ArraySegment<T>可以和数组之间建立一个映射,直接针对数组的某一片段进行操作,其操作后的结果会直接反映在数组上,反之数组上的变化也会反映到数组段上。我们来看看具体的使用吧:
ArraySegment<int> Test = new ArraySegment<int>(arr, 1, 4);
上述例子,表示Test,从arr[1]开始引用了4个元素。Test.Offset就表示第一个引用的元素,也就是arr[1]。
2、Array类
我们之前使用方括号声明数组,实际上就是隐式的使用了Array类。换一个角度看,我们使用的,例如:int[], double[] 我们都可以把他们看成是派生自Array的子类,这样我们可以使用Array为数组定义方法和属性。
<1>创建数组
Array是抽象类,所以不能实例化。但是可以使用静态方法CreateInstance()来创建数组。因为CreateInstance()有多个重载版本,我们就其中一个为例:
//创建一个int型,长度为5的数组,Test Array Test = Array.CreateInstance(typeof(int), 5); //我们将Test[3]的值,赋值为5 Test.SetValue(5, 3); //我们要返回 Test[3]的值 Test.GetValue(3); //将它变为int[]的数组 int[] T1 = (int[])Test;
<2>复制数组
我们可以使用Clone()方法来复制数组,但是如果数组是引用类型那么就只能复制对方的引用。如果数组是值类型,那么才能完整的将对方复制过来。我们还可以使用Copy()方法创建浅表副本。
注意:Clone()和Copy()最大的区别:Copy()方法必须使用与原数组相同阶数且有足够的元素空间,但是Cone()方法会创建一个和原数组等大的数组。
<3>排序
Array类还提供了QuickSort排序算法。使用Sort()方法可以对数组进行排序。但是使用Sort()方法需要实现IComparable接口(.Net已经为基本数据类型实现了IComparable接口,默认从小到大)。对于自定义类型,我们就必须实现IComparable<T>接口,这个接口只用一个方法CompareTo()。如果两者相等,就返回0。如果该实例在参数对象的前面,就返回小于0的值,反之就返回大于0的值。
我们也可以是通过实现IComparer<T>和IComparer接口。我们现在着重看看这个和IComparable接口的区别:
①IComparable在要比较对象的类中实现,可以比较该对象和另一个对象。
②IComparer要在单独一个类中实现,可以比较任意两个对象。
3、枚举
在foreach语句中使用枚举,可以迭代集合中的元素,而且不需要知道集合中的元素个数。foreach语句使用了一个枚举器,我们需要实现IEnumerable接口就可以使用foreach来迭代集合。(数组和集合已经默认实现了IEnumerable接口)。
<1>foreach原理 和 IEnumerator 接口
foreach使用了IEnumerator接口的方法和属性。
//per为Person类的对象 foreach(var p in per) { Consle.WriteLine(p); }
C#编译器会将这段代码解析为
IEnumerator<Person> em = per.GetEnumerator(); while(em.MoveNext()) { Person p = em.Current; Console.WriteLine(p); }
IEnumerator接口的MoveNext()方法作用是:移动到集合的下一个元素,如果有则返回true,否则为false。Current属性为当前的值。
<2>yield语句
由于创建枚举器的工作过于繁琐,于是我们就引入了yield语句,来帮助我们减轻工作量,yield return 返回集合的一个元素,yield break 可停止迭代。
下面我们可以通过一个简单的例子,来了解yield的用法:
public class TFoo { public IEnumerator<string> GetEnumerator() { yield return “Hello”; yield return “World”; } }
现在我们通过foreach迭代集合
int cnt = 0; //我们用这个来看看集合在foreach中迭代了几次 var Test = new TFoo(); foreach(var s in Test) { cnt++; Console.WriteLine(s); }
最后我们可以得到cnt = 2且会输出Hello World。通过这个实例我们就可以大致的了解yield的工作方式。在之前学习泛型的时候我们在链表中已经使用过一次yield了。
注意:yield不能出现在匿名方法中
4、元组(Tuple)
数组是为了处理大量的同类型数据,那么我们要对不同类型的数据可以用什么类似的方法处理吗?当然,为此我们就引入了Tuple类。.Net中定义了8个泛型Tuple类,和一个静态的Tuple。例如:Tuple<T1>包含一个类型为T1的元素,Tuple<T1,T2>则包含两个类型为T1,T2的元素,依次类推。
如果元组元素超过8个那么第8个就可以使用Tuple类定义,例如:
Tuple<T1, T2, T3, T4, T5, T6, T7, TRest> //TRest为另一个元组
我们通过这样的方法就可以创建带任意多个的元组了。
我们使用Create()方法创建元组,例如:
var Test = Tuple.Create<int,int>(2,5);
5、结构比较
数组和元组都实现接口IStructuralEquatable 和 IStructuralComparable。这两个接口不仅仅可以用来比较引用,还可以比较内容。因为这些接口是显示实现的,所以在使用时需要把数组和元组强制转化为这个接口。
IStructuralEquatable接口用于比较两个数组或元组是否具有相同的内容。
IStructuralComparable接口用于给数组或者元组排序。
我们用一个实例来简单的认识IStructuralEquatable接口的用法:
public class Test { public int Id { get; set; } public override bool Equals(object obj) { if (obj == null) return base.Equals(obj); else return this.Id == (obj as Test).Id; } }
我们现在再定义两个类内容相同的类对象t1,t2。
var t1 = new Test[2]{new Test{Id = 2}, new Test{Id = 3}}; var t2 = new Test[2]{new Test{Id = 2}, new Test{Id = 3}};
如果我们直接使用“==”或者“!=”比较那么编译器只会把我们的引用进行比较。这是我们就需要用到IStructuralEquatable接口了。
(t1 as IStructuralEquatable).Equals(t2, EqualityComparer<Test>.Default)
这样我们比较的就是t1,t2的内容了,因为是内容的比较所以它们将会返回True。EqualityComparer<Test>.Default调用的是Test默认的Equals()方法,所以我们只要重写它默认的Equals()方法,给重写的Equals()方法类内容比较的规则,那么我们就可以比较类对象间,是否具有相同的内容。
对于元组e1,e2,我们直接使用e1.Equals(e2)我们就可以比较元组间的内容,但是同样的如果使用比较运算符“==”和“!=”我们还是只能比较他们的引用。
(如有错误,欢迎指正,转载请注明出处)