您的位置:首页 > 编程语言 > C#

《C#高级编程》【第六章】数组 -- 学习笔记

2015-06-15 07:22 281 查看
 为了解决大量的同类型元素,于是数组就孕育而生了。数组是具有一定顺序关系的若干对象的集合体,一维数组可以看作是定长的线性表。反之,n为的数组可以看作线性表的推广。从存储结构上来看,数组是一段连续的存储空间。现在我们看看在C#中的数组:



1、普通数组

在C#中普通数组又可以分为一维数组、多维数组和锯齿数组。

<1>一维数组

我们现在先看看一维数组的声明语法:

类型[] 变量名;

知道怎么声明了,现在我们继续看看数组的初始化吧,在C#中有4种初始化的方式:

//n为数组长度,an为数组内部元素
类型[] 数组名 = new 类型
;  
//为数组分配内存,但是没有赋初值(vs会自动为其赋初值为0)
类型[] 数组名 = new 类型
{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为锯齿数组的维度,后一个方括号为空

我们用一个具体实例来说看看他的使用方法:

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)我们就可以比较元组间的内容,但是同样的如果使用比较运算符“==”和“!=”我们还是只能比较他们的引用。

(如有错误,欢迎指正,转载请注明出处)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: