07
2012
09

C#学习基础数组和元组

单维数组

  当需要操作一系列类型相同的对象的时候,可以使用数组。数组就是一组包含相同类型元素的数据结构。数组的简单声明如下

int[] myArray;  在数组声明以后,内存要分配出来能够存储所有元素的空间。数组是引用类型,因此堆上必须分配出来相应的空间。可以通过初始化数组变量的类型和数组元素的数量来初始化内存分配,如

myArray=new int[4]  

 

数组在初始化大小之后就不能再改变,如果需要可变大小的数组,那么推荐使用集合。还可以直接在声明变量的时候直接声明数组的内容,这个时候内存可以直接分配数组的空间,直接声明可以有如下几种方法:

int[] myArray=new int[] {4,7,11,3};
int[] myArray={4,7,11,3}  在声明数组后,只能通过下标来获取相应的值,下标只能是整数,从0开始,如果使用了一个没有对应值的下标来获取对象,那么将会抛出IndexOutOfRangeException异常

int[] myArray = new int[] {4, 7, 11, 2};
int v1 = myArray[0]; // read first element
int v2 = myArray[1]; // read second element
myArray[3] = 44; // change fourth element  如果不知道数组中有哪些元素,那么可以使用for或者foreach来历遍数组


for (int i = 0; i < myArray.Length; i++)
{
  Console.WriteLine(myArray[i]);
}

foreach (var val in myArray)
{
  Console.WriteLine(val);
}  除了用来声明内置的元素数组,还可以声明自定义类型的数组,如下:


public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public override string ToString()
  {
    return String.Format("{0} {1}", FirstName, LastName);
  }
}

Person[] myPersons = new Person[2];
myPersons[0] = new Person { FirstName="Ayrton", LastName="Senna" };
myPersons[1] = new Person { FirstName="Michael", LastName="Schumacher" };

//Or you can use this way to initialize your array
Person[] myPersons2 =
{
  new Person { FirstName="Ayrton", LastName="Senna"},
  new Person { FirstName="Michael", LastName="Schumacher"}
};  在使用的时候需要注意你的数组承载的是不是引用类型,如果是引用类型,那么内存还要为每个数组元素分配空间,如果引用的元素没有分配内存,那么会抛出NullReferenceExcepiton异常。如下图

 

 

多维数组

  通过使用逗号可以声明多维数组,同样可以用不同的方法声明数组,内存的分配方式还是一样的,而且在声明数组的时候不能在中间留空值:


int[,] twodim = new int[3, 3];
twodim[0, 0] = 1;
twodim[0, 1] = 2;
twodim[0, 2] = 3;
twodim[1, 0] = 4;
twodim[1, 1] = 5;
twodim[1, 2] = 6;
twodim[2, 0] = 7;
twodim[2, 1] = 8;
twodim[2, 2] = 9;

int[,] twodim = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};  在声明了数组之后,大小不可以改变,可以使用两个逗号来声明三维的数组

int[,,] threedim = {
{ { 1, 2 }, { 3, 4 } },
{ { 5, 6 }, { 7, 8 } },
{ { 9, 10 }, { 11, 12 } }
};
Console.WriteLine(threedim[0, 1, 1]);不规则数组

  在声明不规则数组的时候,前面的方括号内定义了行的数目,后面的括号留空,因为每行的元素都不一样,然后再为每一行指定元素

int[][] jagged = new int[3][];
jagged[0] = new int[2] { 1, 2 };
jagged[1] = new int[6] { 3, 4, 5, 6, 7, 8 };
jagged[2] = new int[3] { 9, 10, 11 };  要历遍不规则数组的所有元素,需要使用多层循环,第一层循环历遍行,第二层历遍每一行中的数据


for (int row = 0; row < jagged.Length; row++)
{
  for (int element = 0; element < jagged[row].Length; element++)
  {
    Console.WriteLine("row: {0}, element: {1}, value: {2}", row, element, jagged[row][element]);
  }
}数组类

  上面的语法通过使用扩来来定义数组,创建了一个继承自抽象基类Array的对象,这样就可以使用Array提供的方法和属性。例如,使用Length属性返回数组的长度,或者通过使用GetEnumerator()方法完成了foreach历遍。

  创建数组:Array类是个抽象类,因此不可以通过构造函数创建Array对象。但是,除了使用上面的语法,还可以通过调用Array的静态函数CreateInstance()来创建数组,如果事前不知道数组的类型,那么这个方法很有帮助,第一个参数提供数组的类型,第二个参数提供数组的大小,如下面的例子


Array intArray1 = Array.CreateInstance(typeof(int), 5);
for (int i = 0; i < 5; i++)
{
  intArray1.SetValue(33, i);
}
for (int i = 0; i < 5; i++)
{
  Console.WriteLine(intArray1.GetValue(i));
}  当然,你也可以把它转译成上面使用的int[],如

int[] intArray2 = (int[])intArray1;  CreateInstance()方法有很多重载来创建多维数组,也可以创建不是基于0下标的数组。下面的例子创建了二维的2X3元素,第一维是基于1下标的,第二维是基于10下标的

int[] lengths = { 2, 3 };
int[] lowerBounds = { 1, 10 };
Array racers = Array.CreateInstance(typeof(Person), lengths, lowerBounds);  可以使用SetValue()方法来为每个维度的数组赋值


racers.SetValue(new Person
{
  FirstName = "Alain",
  LastName = "Prost"
}, 1, 10);
racers.SetValue(new Person
{
  FirstName = "Emerson",
  LastName = "Fittipaldi"
}, 1, 11);
racers.SetValue(new Person
{
FirstName = "Ayrton",
LastName = "Senna"
}, 1, 12);
racers.SetValue(new Person
{
  FirstName = "Ralf",
  LastName = "Schumacher"
}, 2, 10);
racers.SetValue(new Person
{
  FirstName = "Fernando",
  LastName = "Alonso"
}, 2, 11);
racers.SetValue(new Person
{
  FirstName = "Jenson",
  LastName = "Button"
}, 2, 12);  虽然下标不是基于0,但是使用的方法都是一样的,唯一需要注意的是下标不要超出界限

  数组的复制:因为数组是引用类型,因此把一个数组变量赋值给另外一个数组变量的时候仅仅是把引用复制给了变量。复制数组的时候,数组集成了ICloneable接口,Clone方法提供了浅复制的实现,当数组是值类型的时候,Copy方法采用复制值的方法,如果是引用类型,那么就是复制引用给新的对象:

int[] intArray1 = {1, 2};
int[] intArray2 = (int[])intArray1.Clone();

  除了使用Clone方法,还可以使用Array.Copey()方法,同样也是实现浅复制,但是他们之间的区别是Clone是创建一个新的数组,而Copy需要传递一个维数和大小与被复制的数组一样的数组。如果想要实现深复制,需要历遍数组然后创建新的对象。

  排序:Array类使用快速排序法来进行数组元素的排序。Sort()方法需要元素继承自IComparable接口,简单的数据类型如System.String和System.Int32继承自这个接口,因此你可以对这些元素的数组进行排序:


string[] names = {
"Christina Aguilera",
"Shakira",
"Beyonce",
"Gwen Stefani"
};
Array.Sort(names);
foreach (var name in names)
{
  Console.WriteLine(name);
}  如果你需要对自定义的类型进行排序,那么这个类型必须继承自IComparable接口,这个接口只有一个方法CompareTo(),如果被比较的值相等返回0,如果被传入的值应该放到这个值的后面,那么返回小于0的值,如果传入的值应该放到这个值的前面,那么返回大于0的值。如下面的例子:


public class Person: IComparable<Person>
{
  public int CompareTo(Person other)
  {
    if (other == null) throw new ArgumentNullException("other");
    int result = this.LastName.CompareTo(other.LastName);
    if (result == 0)
    {
      result = this.FirstName.CompareTo(other.FirstName);
    }
    return result;
  }
}

Person[] persons = {
  new Person { FirstName="Damon", LastName="Hill" },
  new Person { FirstName="Niki", LastName="Lauda" },
  new Person { FirstName="Ayrton", LastName="Senna" },
  new Person { FirstName="Graham", LastName="Hill" }
};
Array.Sort(persons);
foreach (var p in persons)
{
  Console.WriteLine(p);
}  如果你没有机会更改数组类型,或者想要用不同的方式进行比较,那么你可以通过继承ICompare或者ICompare<T>接口。这两个接口都有Compare()方法,被比较的类型一定要继承自这两个接口中的一个。ICompare接口与被比较的类型是独立的,因此Compare()方法定义了两个参数用来进行比较。返回的结果的方法同IComparable接口的CompareTo()方法相似,如下面的示例:


public enum PersonCompareType
{
  FirstName,
  LastName
}
public class PersonComparer: IComparer<Person>
{
  private PersonCompareType compareType;
  public PersonComparer(PersonCompareType compareType)
  {
    this.compareType = compareType;
  }
  public int Compare(Person x, Person y)
  {
    if (x == null) throw new ArgumentNullException("x");
    if (y == null) throw new ArgumentNullException("y");
    switch (compareType)
    {
      case PersonCompareType.FirstName:
        return x.FirstName.CompareTo(y.FirstName);
      case PersonCompareType.LastName:
        return x.LastName.CompareTo(y.LastName);
      default:
        throw new ArgumentException("unexpected compare type");
    }
  }
}  现在,你就可以通过传递PersonComparer对象作为第二个参数来对Array进行排序了:

Array.Sort(persons,new PersonComparer(PersonCompareType.FirstName));
foreach (var p in persons)
{
  Console.WriteLine(p);
}  Array类的Sort方法还有一个传递委托的比较方法,使用委托参数来传递一个用于比较两个类型的方法,从而不用依赖于IComparable或者ICompare接口

使用数组作为参数

  Array也可以被用作返回类型或者方法的参数,如下面的例子:


static Person[] GetPersons()
{
  return new Person[] {
    new Person { FirstName="Damon", LastName="Hill" },
    new Person { FirstName="Niki", LastName="Lauda" },
    new Person { FirstName="Ayrton", LastName="Senna" },
    new Person { FirstName="Graham", LastName="Hill" }
  };
}

static void DisplayPersons(Person[] persons)
{
//.. .
}  数组协变:数组支持协变,着意味着可以在传递参数的时候使用基类类型或者元素继承自的类型来定义数组,如下

static void DisplayArray(object[] data)
{
  //...
}但是数组协变只支持引用类型,不支持值类型。数组协变有一个bug,就是只有在运行的时候才会抛出异常,例如上面的例子中,你传递一个Person的数组作为object数组,在写代码的时候,这个object数组可以接受任意继承自object类型的成员,编译器不会报错,例如传递字符串类型到这个数组编译器是会通过编译的,但是因为实际上这是一个Person类型的数组,因此在运行的时候会报告错误。

  ArraySegment<T>:ArraySegment<T>代表一个数组的一段。如果仅需要传递或者返回数组的一段,那么可以使用这个类型,在传递的时候只需要指出这个段的起始下标和长度就可以,如下面的代码:


static int SumOfSegments(ArraySegment < int > [] segments)
{
  int sum = 0;
  foreach (var segment in segments)
  {
    for (int i = segment.Offset; i < segment.Offset + segment.Count; i++)
    {
      sum += segment.Array[i];
    }
  }
  return sum;
}

int[] ar1 = { 1, 4, 5, 11, 13, 18 };
int[] ar2 = { 3, 4, 5, 18, 21, 27, 33 };
var segments = new ArraySegment < int > [2]
{
  new ArraySegment < int > (ar1, 0, 3),
  new ArraySegment < int > (ar2, 3, 3)
};
var sum = SumOfSegments(segments);  需要注意的是数组段并没有复制起始的数组,相反,如果在函数中数组段的元素改变了,那么在原来的起始数组里面是可以被反映出来的。

枚举

  通过使用foreach语句可以在不知道集合或者数组内部元素的情况下进行历遍,foreach语句使用了枚举器,下图展示了一个客户端中foreach的执行过程。数组或者集合元素继承自IEnumerable接口,实现了GetEnumerator方法,GetEnumerator方法返回了一个继承自IEnumerator接口的枚举器。那么IEnumerator接口被用来历遍集合或者数组。GetEnumerator()方法定义在IEnumerable接口中,但是foreach语句并不一定需要元素继承自IEnumerable解耦,只要元素内部有定义GetEnumerator()方法,并且返回继承自了IEnumerator接口的元素就可以。

 

  IEnumerator接口:foreach语句是使用IEnumerator接口中的方法和属性来进行历遍的,例如Current和MoveNext()。他的泛型IEnumerator<T>继承自IDisposable,需要实现Dispose()方法来清理内存。IEnumerator接口还定义了Reset()方法,用来COM操作。但是很多.NET的的枚举器在继承的时候都是抛出NotSupportedException。

  foreach语句:foreach语句并不是直接翻译成IL代码,在C#中他首先被翻译成了使用GetEnumerator和MoveNext的while循环,例如下面的代码就是foreach语句被翻译之前和之后的样子:


foreach (var p in persons)
{
  Console.WriteLine(p);
}

IEnumerator <Person  enumerator = persons.GetEnumerator();
while (enumerator.MoveNext())
{
  Person p = enumerator.Current;
  Console.WriteLine(p);
}  yield声明:在C#的第一个版本中就已经有了foreach语句,但是C#1.0中创建一个枚举器非常麻烦,在C#2.0中添加了yield声明用来创建枚举器,使其更加方便。yeld返回语句返回集合中的一个元素并且将当前的位置移动到下一个,yield break语句中段历遍。如下面的例子:


using System;
using System.Collections;
namespace Wrox.ProCSharp.Arrays
{
  public class HelloCollection
  {
    public IEnumerator <string> GetEnumerator()
    {
      yield return "Hello";
      yield return "World";
    }
  }}  包含yield声明的方法或者属性通常被叫做历遍块,一个历遍块必须返回一个IEnumerator或者IEnumerable接口,或者他们的泛型。这个块可以包含多个yield return或者yield break声明,但是不能包含return语句。现在就可以通过foreach语句来历遍这个集合了


public void HelloWorld()
{
  var helloCollection = new HelloCollection();
  foreach (var s in helloCollection)
  {
    Console.WriteLine(s);
  }
}  在历遍块的内部,编译器会生成一个yield类型,这个yield类型集成了IEnumerable和IEnumerator的属性和方法,在下面的例子中Enumerator就是这个yield类型:


public class HelloCollection
{
  public IEnumerator GetEnumerator()
  {
    return new Enumerator(0);
  }
  public class Enumerator: IEnumerator<string>, IEnumerator, IDisposable
  {
    private int state;
    private string current;
    public Enumerator(int state)
    {
      this.state = state;
    }
    bool System.Collections.IEnumerator.MoveNext()
    {
    switch (state)
    {
      case 0:
        current = "Hello";
        state = 1;
        return true;
      case 1:
        current = "World";
        state = 2;
        return true;
      case 2:
        break;
    }
    return false;
  }
  void System.Collections.IEnumerator.Reset()
  {
    throw new NotSupportedException();
  }
  string System.Collections.Generic.IEnumerator<string>.Current
  {
    get
    {
      return current;
    }
  }
  object System.Collections.IEnumerator.Current
  {
    get
    {
      return current;
    }
  }
  void IDisposable.Dispose()
  {
  }
}  记住yield声明生成一个历遍器,不仅仅是一个元素的列表。历遍器被foreach语句调用。当元素通过foreach一个一个拿到之后,历遍器依然仍然存在,这样就可以在历遍巨型数组的时候不用把所有数据一次性全部读入内存。

  历遍集合的不同方法:下面的代码演示了如何正向、反向历遍,以及历遍数组的一部分的代码:


public class MusicTitles
{
  string[] names = { "Tubular Bells", "Hergest Ridge", "Ommadawn","Platinum" };
  public IEnumerator < string > GetEnumerator()
  {
    for (int i = 0; i < 4; i++)
    {
      yield return names[i];
    }
  }
  public IEnumerable < string > Reverse()
  {
    for (int i = 3; i > = 0; i -- )
    {
      yield return names[i];
    }
  }
  public IEnumerable < string > Subset(int index, int length)
  {
    for (int i = index; i < index + length; i++)
    {
      yield return names[i];
    }
  }
}
var titles = new MusicTitles();
foreach (var title in titles)
{
  Console.WriteLine(title);
}
Console.WriteLine();
Console.WriteLine("reverse");
foreach (var title in titles.Reverse())
{
  Console.WriteLine(title);
}
Console.WriteLine();
Console.WriteLine("subset");
foreach (var title in titles.Subset(2, 2))
{
  Console.WriteLine(title);
}  通过Yield Return返回枚举器:通过yield语句你可以做更多复杂的事情,例如返回枚举器:


public class GameMoves
{
  private IEnumerator cross;
  private IEnumerator circle;
  public GameMoves()
  {
    cross = Cross();
    circle = Circle();
  }
  private int move = 0;
  const int MaxMoves = 9;
  public IEnumerator Cross()
  {
    while (true)
    {
      Console.WriteLine("Cross, move {0}", move);
      if (++move >= MaxMoves)
        yield break;
      yield return circle;
    }
  }
  public IEnumerator Circle()
  {
    while (true)
    {
      Console.WriteLine("Circle, move {0}", move);
      if (++move >= MaxMoves)
        yield break;
      yield return cross;
    }  }}var game = new GameMoves();
IEnumerator enumerator = game.Cross();
while (enumerator.MoveNext())
{
  enumerator = enumerator.Current as IEnumerator;
}元组

  数组包含了相同类型的元素,元组可以包含不同类型的元素。.NET 4定义了支持八个元素的Tuple的泛型类和一个静态的Tuple类作为工厂。不同的泛型Tuple类支持不同元素个数的类型,如Tuple<T1>包含一个成员,Tuple<T1,T2>包含两个成员:

public static Tuple<int, int> Divide(int dividend, int divisor)
{
  int result = dividend / divisor;
  int reminder = dividend % divisor;
  return Tuple.Create<int, int>(result, reminder);
}var result = Divide(5, 2);
Console.WriteLine("result of division: {0}, reminder: {1}",
result.Item1, result.Item2);  虽然Tuple的泛型类最多支持八个元素的定义函数,但是,你可以将第八个元素这职位另外一个Tuple类,这样就可以创建任意多个元素的元组,例如:

var tuple = Tuple.Create<string, string, string, int, int, int, double, Tuple<int, int>>(
"Stephanie", "Alina", "Nagel", 2009, 6, 2, 1.37, Tuple.Create<int, int>(52, 3490));结构比较

  数组和元组都集成了IStructuralEquatable和IStructuralComparable。 这些接口是显示实现的,因此必须要将数组和元组转换成接口才能使用。IStructuralEquatable用来比较两个元组或者数组的内容是否相同,IStructuralComparable用来对元组或者数组进行排序。

  在下面的示例中,首先定义了一个Person类,继承自IEquatable接口:


public class Person: IEquatable<Person>
{
  public int Id { get; private set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public override string ToString()
  {
    return String.Format("{0}, {1} {2}", Id, FirstName, LastName);
  }
  public override bool Equals(object obj)
  {
    if (obj == null) throw new ArgumentNullException("obj");
    return Equals(obj as Person);
  }
  public override int GetHashCode()
  {
    return Id.GetHashCode();
  }
  public bool Equals(Person other)
  {
    if (other == null) throw new ArgumentNullException("other");
      return this.Id == other.Id && this.FirstName == other.FirstName &&
    this.LastName == other.LastName;
  }
}  下面定义了两个数组,数组中包含一个相同引用的元素,和一个值相同的元素,使用==比较的是数组的引用:


var janet = new Person { FirstName = "Janet", LastName = "Jackson" };
Person[] persons1 = {
  new Person
  {
    FirstName = "Michael",
    LastName = "Jackson"
  },
  janet
};
Person[] persons2 = {
  new Person
  {
    FirstName = "Michael",
    LastName = "Jackson"
  },
  janet
};
if (persons1 != persons2)
  Console.WriteLine("not the same reference");  要想通过调用自定义的比较方法,需要使用Equals方法,这个方法需要两个参数,第一个参数是被比较的对象,第二个参数是IEqualityComparer类型。默认的循环ixianshiEqualityComparer<T>类,这个时候会检查这个类型是否集成子IEquatable,然后激活IEquatable.Equals()费那个发,如果没有继承自IEquatable,那么Equals方法会调用对象的基类中的比较方法,例如:

if ((persons1 as IStructuralEquatable).Equals(persons2,
EqualityComparer<Person>.Default))
{
  Console.WriteLine("the same content");
}  下面是使用类似的方法对元组进行比较:

var t1 = Tuple.Create<int, string>(1, "Stephanie");
var t2 = Tuple.Create<int, string>(1, "Stephanie");
if (t1 != t2)
  Console.WriteLine("not the same reference to the tuple");  Tuple<T>类提供了两个Equals()方法,第一个是使用object基类中的方法,参数为一个object类型;第二个是IStructuralEqualityComparer接口定义的,参数是一个object对象和一个IEqualityComparer。下面这个方法使用的是EqualityComparer<object>.Default方法来获得一个ObjectEqualityComparer<objcet>用来进行比较,这样的话每个元素都是调用Object.Equals()方法:

if (t1.Equals(t2))
  Console.WriteLine("the same content");  同样,可以创建一个自定义的IEqualityComparer,如下面的示例:


class TupleComparer: IEqualityComparer
{
  public new bool Equals(object x, object y)
  {
    return x.Equals(y);
  }
  public int GetHashCode(object obj)
  {
    return obj.GetHashCode();
  }
}if (t1.Equals(t2, new TupleComparer()))
  Console.WriteLine("equals using TupleComparer");

« 上一篇下一篇 »

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。