C#之数组
@(Net)[C#|Array]
数组 是允许将多个数据项作为集合来处理的一种机制。CLR支持一维(如果此时数组索引从0开始,也被称为SZ(single-dimension,zero-base)、向量(Vector))、多维和交错数组(有数组构成的数组)。所有的数组类型都隐式的从System.Array抽象类派生,而后者又派生自System.Object。这意味着数组始终是引用类型,在托管堆上分配。在应用程序的变量或字段中,包含的是数组的引用,而不是包含数组本身的元素。
## C#数组的声明与初始化
当使用C#语法声明一个数组时,CLR会在内部将它转换为Array的子类——合成一个对应该数组维数和元素类型的伪类型。这个伪类型实现了类型化的泛型集合接口,如IList
在C#中,我们初始化数组的方式大致可以概括为两种,如下:
string[] s = new []{"hello","world"};//使用数组初始化器初始化一个数组(亦可只要大括号内部的内容)
var sa = new[]{"hello","world"};//使用隐式类型初始化一个数组,此时不能省略前面的new[]
除去上面静态创建的方式,还可以使用 Array.CreateInstance
方法在运行时动态的创建一个数组的实例,该方法需要制定数组中元素的类型及个数。
下面动态的创建一个长度为2的字符串数组。
Array arr = Array.CreateInstance(typeof(string), 2);
//赋值
arr.SetValue("hello", 0);
arr.SetValue("world", 1);
//取值
arr.GetValue(0);
//将其转换为静态数组
string[] cs_arr = (string[])arr;
## 一维0基数组 多数情况下,我们创建的数组都是0基数组(C#亦可以调用Array类的静态方法CreateInstance()创建非0基的数组,可以在运行时指定数组的元素类型、维数、和每一维开始的数组的下界,但这样的数组不符合CLS(Common Language Specification)),访问0基一维数组要比访问多维数组或非0基数组要快一点。这其中的原因有一下几方面:
1. 针对0基一维数据,具有一些特殊的IL指令,如 newarr(将新的0基一维数组的对象引用推送到堆栈上)、ldlen(将0基一维数组的数组元素的数目推送到计算堆栈上)、stelem(用计算堆栈中的值替换0基一维数组中指定索引位置的数组元素)、ldelema(将指定索引位置的数组元素的地址(托管指针)推送到计算堆栈的顶部)等,这些IL指令会导致JIT编译器生成优化代码。例如,JIT编译器生成的代码假定数组是0基的,那么在访问数组是就不需要从指定索引中减去一个偏移量了。
2. 这一方面我们举个例子来说明
int[] int_arr = {1,2,3,4,5};
for(int i=0;i<int_arr.Length;i++){
//执行对int_arr[i]的操作
}
在上述的for循环表达式中调用了数组的 Length属性,但我们知道实际上是调用的查询长度的方法(C#中的属性最终是以Set_propertyName和Get_propertyName的形式存在的,查询IL可以看出),JIT编译器知道Length是Array类的属性,所以在生成的代码中实际上只调用了该属性一次,然后将结果存储到一个临时变量中,每次循环迭代检查的都是这个临时变量。这样就加快了JIT编译代码的速度。这在访问非0基数组和多维数组时,JIT编译器不会讲索引检查从循环迭代中拿出来,索引每次都要验证指定的索引,具体来说就是生成类似如下的代码来检查(0>=arr.GetLowerBound(int dimensino))&&((Length-1)<=arr.GetUpperBound(int dimension))是否成立。如果很关心性能的话,使用交错数组(数组构成的数组)来代替矩形数组(多维数组)是个不错的选择。
数组实现的集合访问接口
所有的C#数组都实现了IEnumerable、IList、ICollection(注意这里实现的是非泛型类型的集合操作接口),这三个接口的定义如下:
IEnumerable:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
实现了IEnumerable接口的类支持枚举
IList:
public interface IList : ICollection, IEnumerable
{
int Add(object value);
void Clear();
bool Contains(object value);
int IndexOf(object value);
void Insert(int index, object value);
void Remove(object value);
void RemoveAt(int index);
}
实现IList接口的类支持基于索引的操作,在Array中,这些方法都是显式实现的,因此,如果一个数组想要调用其中的某个方法,必须转换为IList接口类型。
public interface ICollection:IEnumerable{
void CopyTo(Array arr,int index);
int Count{get;}
}
实现了ICollectiond的类具有Count属性,因其还继承自IEnumerable,故还可枚举。
前面我们说过Array类并没有实现上述集合接口对应的泛型版本,但当我们创建一个数组时,只要该数组中包含的元素为引用类型的,CLR会自动的为其数组类型及其所有基类实现IEnumerable<T>、ICollection<T>、IList<T>
接口,下面我们举例说明。
现有A和B两个类,其中B是A的子类,As和Bs是类型A、B对应的类型数组,则:
Console.WriteLine(Bs is IList<B>);//True
Console.WriteLine(Bs is ICollection<B>);//True
Console.WriteLine(Bs is IEnumerable<B>);//True
Console.WriteLine(As is IList<A>);//True
Console.WriteLine(As is ICollection<A>);//True
Console.WriteLine(As is IEnumerable<A>);//True
但如果数组中包含的元素是值类型的,则不会为其基类实现接口对应的泛型版本(所有的值类型都继承自System.ValueType和System.Object)
数组方法
#### 1.查找
- 快速搜索排序数组中的特定元素(一维数组)
1.public static int IndexOf(Array array, object value)
在数组array中查找指定元素value,如果存在就返回其索引值,如果不存在就返回-1.,该方法还有多个重载,可以指定查找开始的索引(startIndex)和查找的数组元素的个数(count),在这里必须满足
startIndex - count+1>=0,否则会出现
System.ArgumentOutOfRangeException` 的异常。
2.public static int LastIndexOf(Array array,object value)
作用和用法与IndexOf一样,只不过LastIndexOf的查找是从后往前查的。
- 搜索未排序数组汇总特定的元素
1.public static T Find <T>(T[] array,Predicate<T> match)
在数组array中查找匹配条件match的第一个数组元素并返回,如果不存在满足匹配条件的项,就返回T类型的默认值。如果要返回满足匹配条件的所有项,可以使用 *** public static T[] FindAll<T>(T[] array, Predicate<T> match)***,参数中的Predicate<T>表示一个具有一个T类型参数返回类型为bool的泛型委托。如果不存在则返回一个长度为0的T[].
string[] test = {"hello","girl","I","Love","you","and","you","?"};
string[] result = Array.FindAll(test,s=>s.Contain("o"));//找出所有包含o的字符串
2.*public static T FindLast <T\>(T[] array,Predicate<T\> match)*的功能和用法与Find相同,只不过是从后向前查找的,既返回最后一个匹配条件的数组元素。
3.public static int FindIndex/FindLastIndex <T>(T[] array, Predicate<T> match)
在数组array中查找匹配条件mathc的第一个数组元素返回其索引值。如果不存在匹配条件的项就返回-1,该方法具有多个重载,重载的参数形式类似于IndexOf。
4.public static bool Exists<T>(T[] array, Predicate<T> match)
判断数组array中是否存在匹配条件的项,如果存在返回True,否则返回False(内部调用IndexOf方法实现)。
5.public static bool TrueForAll%lt;T>(T[] array, Predicate<T> match)
判断数组array中的所有项是否都满足匹配条件match,如果是,返回true,否则,返回False。
6.public static int BinarySearch<T>(T[] array,int index,int length,T value,IComparer<T>comparer)
二分法查找,速度较快,但只适合于排序数组,而且要求元素能够比较排序该方法有多个重载,这个是参数最多的一个,其中array表示要搜索的要从零开始的一维排序数组,index表示要搜索的范围的起始索引。length表示要搜索的范围的长度,value表示要搜索的值,comparer表示比较元素时要使用的 IComparer
2. 数组排序
Array内置以下排序方法:
- public static void Sort<T>(T[] array)
- public static void Sort(Array array)
- public static void Sort<Tkey,Tvalue>(Tkeys[] keys,TValue[] values)
- public static void Sort(Array keys,Array values)
上述的每个方法重载后都可以接受:
1.int index //排序开始的位置
2.int length //排序的元素个数
3.*IComparer/IComparer<T> //判断排序的对象
4.Comparison<T> //执行排序的代码,只是一个Comparison<T>类型的委托,该委托的定义如下:
public delegate int Comparison<T>(T x,T y)
Array.Sort要求数组中的元素都实现IComparable接口(C#中的基元类型(能够被编译器直接识别的类型,C#中的每种基元类型都可以映射为FCL中的一种类型,如基元类型int对应着FCL中的System.Int32)都实现了该接口),,如果数组中的元素是不可比较的,或者希望重写默认的顺序比较,那么必须给Sort提供上述重载中的 3 或*4类型的参数。
下面我们简单举例说明一下它的用法:
- 接收一对数组的方法
int[] numbers = {3,2,1};
string[] words = {"three","two","one"};
Array.Sort(numbers,words);
执行完Sort方法后,numbers变为{1,2,3},words变为{"one","two","three"},这种排序方法实际上是通过调整每个数组的元素及基于第一个数组的顺序判断而实现的。
注意:Array.Sort方法的排序是在原数组上操作的,作为Sort的替代方法也可以使用LINQ(Language Integrated Query,集成语言查询)的OrderBy和ThenBy方法来实现,但LINQ的方法不会修改原数组,而是将排序结果保存在一个新的IEnumerable<T>序列中返回。
#### 3. 倒转数组元素
通过Array的Reverse方法可以倒转数组中的所有或部分元素的顺序,该方法的定义如下:
public static void Reverse(Array array);
public static void Reverse(Array array,int index,int length);
#### 4. 数组的复制
Array有4个方法可以进行浅拷贝操作(如果要进行深拷贝必须进行遍历,自行复制):Clone、CopyTo、Copy和ConstrainedCopy,前两个方法时实例方法,后两个是静态方法。
Clone方法返回一个全新的数组(其实返回的是一个object,需要自行进行类型转换),Copy和CopyTo方法复制数组中的若干连续元素(这两个方法都有多个重载,可以指定源数组复制开始位置的索引(sourceIndex),要复制的元素个数(length),目标数组中存储开始位置的索引(destinationIndex))
**注意 *** 复制多维数组需要将多维索引映射为线性索引,例如,一个3x3数组的中间矩阵(position[1,1]),可以用4(13+1)来表示。
ConstraindeCopy执行一个原子操作:如果所有请求的元素都无法成功复制,那么操作就会回滚。
int[] a={1,2,3,4,5};
int[] b = {6,7,8,9,10};
//Array.Copy(sourceArray,sourceIndex,destinationIndex,destinationIndex,length)
Array.Copy(a,1,b,0,4);//此时数组b变为{2,3,4,5,10}
#### 5. 转换和调整大小
Array.ConvertAll会创建和返回一个包含元素类型OutPut类型的新数组,调用所提供的Converter代理(说白了就是一个委托,参数为TInput,返回值为TOutput)就可以复制元素,该方法的定义如下: `public static TOutput[] ConvertAll
```C# string[] s = {"1","2","3","4"}; var b = Array.ConvertAll(s,a=>Int32.Parse(a));//将数组s中的所有字符串都转换为Int32 Console.WriteLine(b.GetType());//System.Int32[] var c = Array.ConvertAll(b,d=>d*d);//c为{1,4,9,16} ```
Array的Resize方法可以改变数组的长度,实现方式其实是创建一个指定长度的新数组,然后将对应长度个数的元素复制到新数组中,在通过引用参数返回这个新数组。然而在其他对象中指向原始数组的任意引用都不变,该方法的定义如下:
public static void Resize<T>(ref T[] array,int newSize)
6. 两个数组的比较
假如现在有两个数组,如下示: ```cs object[] a1 = {1,"2",true}; object[] a2 = {1,"2",true}; ``` 若要判断这两个数组是否相等该如何判断呢?你是否能够判断出下面的比对结果
bool eq1 = a1==a2;
bool eq2 = object.ReferenceEquals(a1,a2);
bool eq3 = a1.Equals(a2);
上面的三个比较的结果均为 false
。那是因为数组本身是引用类型的,上面的三种方式比较的均为引用对象在堆栈中的地址,明显是不相同的。
那么,要比较两个数组是否完全相同,除了遍历外,还有其它的方式吗?答案是肯定的,其方式如下示:
IStructuralEquatable se1 = a1 as IStructuralEquatable;
if(se1!=null)
{
bool eq4 = se1.Equals(a2,StructuralComparisons.StructuralEqualityComparer));//true
}