基础系列(3)—— 高级数据类型
一、数组
(一)简单数组(一维数组)
数组是一种数据结构,它可以包含同一个类型的多个元素。
1.数组的声明
在声明数组时,先定义数组中的元素类型,其后是一对空方括号和一个变量名。
int[] myArray;
2.数组的初始化
声明了数组之后,就必须为数组分配内存,以保存数组的所有元素。数组是引用类型,所以必须给它分配堆上的内存。为此,应使用new运算符,指定数组中元素的类型和数量来初始化数组的变量。
myArray = new int[4];
在声明和初始化数组后,变量myArray就引用了4个整数值,它们位于托管堆上:
在指定了数组的大小后,就不能重新设置数组的大小。如果事先不知道数组中应包含多少个元素,就可以使用集合。除了在两个语句中声明和初始化数组之外,还可以在一个语句中声明和初始化数组:
int[] myArray = new int[4];
还可以使用数组初始化器为数组的每个元素复制。数组初始化器只能在声明数组变量时使用,不能在声明数组之后使用。
int[] myArray = new int[4]{1,3,5,7};
如果用花括号初始化数组,可以不指定数组的大小,因为编译器会自动统计元素的个数:
int[] myArray = new int[]{1,3,5,7};
也可以使用更简单的形式:
int[] myArray = {1,3,5,7};
3.访问数组元素
在声明和初始化数组之后,就可以使用索引器访问其中的元素了。数组只支持有整型参数的索引器。索引器总是以0开头,表示第一个元素。可以传递给索引器的最大值是元素个数减1,因为索引从0开始:
int[] myArray = {1,3,5,7}; int v1 = myArray[0]; int v2 = myArray[1]; myArray[3] = 4;
可以使用数组的Length属性获取元素的个数。
4.数组中使用引用类型
数组除了能声明预定义类型的数组,还可以声明自定义类型的数组。
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" };
如果数组中的元素是引用类型,就必须为每个数组元素分配内存。如果使用了数组中未分配内存的元素,就会抛出NullReferenceException类型的异常。
下面是内存情况:
对自定义类型也可以使用数组初始化器:
Person[] myPersons2 = { new Person { FirstName="Ayrton", LastName="Senna"}, new Person { FirstName="Michael", LastName="Schumacher"} };
(二).多维数组
多维数组用两个或多个整数来索引。在C#中声明多维数组,需要在方括号中加上逗号。数组在初始化时应指定每一维的大小(也称为阶)。
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[4]{3,4,5,6}; jagged[2] = new int[3]{7,8};
迭代锯齿数组中的所有元素的代码可以放在嵌套的for循环中。在外层的for循环中迭代每一行,在内层的for循环中迭代一行中的每个元素:
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类
用方括号声明数组是C#中使用Array类的表示法。在后台使用C#语法,会创建一个派生自抽象基类Array的新类。这样,就可以使用Array类为每个C#数组定义的方法和属性了。
Array类实现的其它属性有LongLength和Rank。如果数组包含的元素个数超出了整数的取值范围,就可以使用LongLength属性来获得元素个数。使用Rank属性可以获得数组的维数。
1.创建数组
Array类是一个抽象类,所以不能使用构造函数来创建数组。但除了使用C#语法创建数组实例之外,还可以使用静态方法CreateInstance()创建数组。如果事先不知道元素的类型,该静态方法就很有用,因为类型可以作为Type对象传递给CreateInstance()方法。
CreateInstance()方法的第一个参数是元素的类型,第二个参数定义数组的大小。可以使用SetValue()方法设置对应元素的值,用GetValue()方法读取对应元素的值。
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); racers.SetValue(new Person { FirstName = "Alain", LastName = "Prost" }, index1: 1, index2: 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 = "Michael", LastName = "Schumacher" }, 2, 10); racers.SetValue(new Person { FirstName = "Fernando", LastName = "Alonso" }, 2, 11); racers.SetValue(new Person { FirstName = "Jenson", LastName = "Button" }, 2, 12); Person[,] racers2 = (Person[,])racers; Person first = racers2[1, 10]; Person last = racers2[2, 12];
2.复制数组
因为数组是引用类型,所以将一个数组变量赋予另一个数组变量,就会得到两个引用同一数组的变量。数组实现ICloneable接口。这个接口定义的Clone()方法会复制数组,创建数组的浅表副本。如果数组的元素是值类型,Clone()方法会复制所有值:
int[] a1 = {1,2}; int[] a2 = (int[])a1.Clone();
如果数组包含引用类型,只复制引用。除了使用Clone()方法之外,还可以使用Array.Copy()方法创建浅表副本。
Person[] beatles = { new Person { FirstName="John", LastName="Lennon" }, new Person { FirstName="Paul", LastName="McCartney" } }; Person[] beatlesClone = (Person[])beatles.Clone(); Person[] beatlesClone2 = new Person[2]; Array.Copy(beatlesClone,beatlesClone2,2);//注意与Clone的语法区别,Copy需要传递阶数相同的已有数组。(还可以使用CopyTo()方法)
3.排序
Array类使用快速排序算法对数组中的元素进行排序。Sort()方法需要数组中的元素实现IComparable接口。因为简单类型(如String,Int32)实现IComparable接口,所以可以对包含这些类型的元素排序。
string[] names = { "Christina Aguilera", "Shakira", "Beyonce", "Gwen Stefani" }; Array.Sort(names); foreach (string name in names) { Console.WriteLine(name); }
如果对数组使用使用自定义类,就必须实现IComparable接口。这个接口只定义了一个方法CompareTo()方法,如果要比较的对象相等,该方法就返回0.如果该实例应排在参数对象的前面,该方法就返回小于i0de值。如果该实例应排在参数对象的后面,该方法就返回大于0的值。
public class Person : IComparable<Person> { public string FirstName { get; set; } public string LastName { get; set; } public override string ToString() { return String.Format("{0} {1}", FirstName, LastName); } 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 (Person p in persons) { Console.WriteLine(p); }
如果Person对象的排序方式与上述不同,或者不能修改在数组中用作元素的类,就可以实现IComparer接口或IComparer<T>接口。这两个接口定义了方法Compare()方法。机型比较的类必须实现这两个接口之一。
public enum PersonCompareType { FirstName, LastName } //通过使用实现了IComparer<T> 泛型接口的PersonComparer类比较Person对象数组。 public class PersonComparer : IComparer<Person> { private PersonCompareType compareType; public PersonComparer(PersonCompareType compareType) { this.compareType = compareType; } #region IComparer<Person> Members 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"); } } #endregion } 客户端代码: 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, new PersonComparer(PersonCompareType.FirstName)); foreach (Person p in persons) { Console.WriteLine(p); }
(五).数组作为参数
数组可以作为参数传递给方法,也可以从方法中返回。
1.数组协变
数组支持协变。这表示数组可以声明为基类,其派生类型的元素可以赋值于数组元素
static void DisPlay(object[] o) { //.. }
可以给该方法传递一个Person[]。数组协变只能用于引用类型,不能用于值类型。
2.ArraySegment<T>
结构ArraySegment<T>表示数组的一段。如果需要使用不同的方法处理某个大型数组的不同部分,那么可以把相应的数组部分复制到各个方法。
ArraySegment<T>结构包含了关于数组段的信息(偏移量和元素个数)。
数组段不复制原数组的元素,但原数组可以通过ArraySegment<T>访问。如果数组段中的元素改变了,这些变化就会反映到原数组中。
static void Main() { 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); Console.WriteLine("sum of all segments: {0}", sum); } 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; }
二、 枚举
(一) 枚举的定义
枚举类型声明为一组相关的符号常数定义了一个类型名称。枚举用于“多项选择”场合,就是程序运行时从编译时已经设定的固定数目的“选择”中做出决定。枚举是由程序员定义的类型,与结构和类具有很强的相似性。
枚举类型(也称为枚举)为定义一组可以赋给变量的命名整数常量提供了一种有效的方法。例如,假设您必须定义一个变量,该变量的值表示一周中的一天。该变量只能存储七个有意义的值。若要定义这些值,可以使用枚举类型。枚举类型是使用 enum 关键字声明的。
默认情况下,枚举中每个元素的基础类型是 int。可以使用冒号指定另一种整数值类型。如果不为枚举数列表中的元素指定值,则它们的值将以 1为增量自动递增。在前面的示例中,Days.Sunday 的值为 0,Days.Monday 的值为 1,依此类推。创建新的 Days 对象时,如果不显式为其赋值,则它将具有默认值 Days.Sunday (0)。创建枚举时,应选择最合理的默认值并赋给它一个零值。这便使得只要在创建枚举时未为其显式赋值,则所创建的全部枚举都将具有该默认值。枚举中大小写敏感,但是建议不要这样。
enum Days { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday };
(二)枚举的优点
1、枚举能够使代码更加清晰,它允许使用描述性的名称表示整数值。
2、枚举使代码更易于维护,有助于确保给变量指定合法的、期望的值。
3、枚举使代码更易输入。
(三) 枚举的说明
- 枚举使用enum关键字来声明,与类同级。枚举本身可以有修饰符,但枚举的成员始终是公共的,不能有访问修饰符。枚举本身的修饰符仅能使用public和internal。
- 枚举是值类型,隐式继承自System.Enum,不能手动修改。System.Enum本身是引用类型,继承自System.ValueType。
- 枚举都是隐式密封的,不允许作为基类派生子类。
- 枚举类型的枚举成员均为静态,且默认为Int32类型。
- 每个枚举成员均具有相关联的常数值。此值的类型就是枚举的底层数据类型。每个枚举成员的常数值必须在该枚举的底层数据类型的范围之内。如果没有明确指定底层数据类型则默认的数据类型是int类型。
- 枚举成员不能相同,但枚举的值可以相同。
- 枚举最后一个成员的逗号和大括号后面的分号可以省略
(四) 枚举的例子
enum TimeOfDay
{
Moning=0,
Afternoon=1,
Evening=2
}
class Program
{
public static string getTimeOfDay(TimeOfDay time)
{
string result = string.Empty;
switch (time)
{
case TimeOfDay.Moning:
result="上午";
break;
case TimeOfDay.Afternoon:
result = "下午";
break;
case TimeOfDay.Evening:
result = "晚上";
break;
default:
result = "未知";
break;
}
return result;
}
static void Main(string[] args)
{
string time= getTimeOfDay(TimeOfDay.Evening);
Console.Write(time);
Console.Read(); //输出晚上
}
(五) 枚举的方法
1、获取枚举字符串
TimeOfDay time = TimeOfDay.Afternoon;
Console.WriteLine(time.ToString());//输出:Afternoon
2、Enum.Parse()方法。
这个方法带3个参数,第一个参数是要使用的枚举类型。其语法是关键字typeof后跟放在括号中的枚举类名。typeof运算符将在第5章详细论述。第二个参数是要转换的字符串,第三个参数是一个bool,指定在进行转换时是否忽略大小写。最后,注意Enum.Parse()方法实际上返回一个对象引用——我们需要把这个字符串显式转换为需要的枚举类型(这是一个取消装箱操作的例子)。对于上面的代码,将返回1,作为一个对象,对应于TimeOfDay.Afternoon的枚举值。在显式转换为int时,会再次生成1。
TimeOfDay time2 = (TimeOfDay) Enum.Parse(typeof(TimeOfDay), "afternoon", true);
Console.WriteLine((int)time2);//输出1
3、得到枚举的某一值对应的名称
lbOne.Text = Enum.GetName(typeof(TimeOfDay), 0);
lbOne.Text = ((TimeOfDay)0).ToString();//返回:Morning
4、得到枚举的所有的值
foreach (int i in Enum.GetValues(typeof(TimeOfDay)))
lbValues.Text += i.ToString();
5、枚举所有的名称
foreach(string temp in Enum.GetNames(typeof(TimeOfDay)))
lbNames.Text+=temp;
三、IEnumerable和IEnumerator
初学C#的时候,老是被IEnumerable、IEnumerator、ICollection等这样的接口弄的糊里糊涂,我觉得有必要切底的弄清楚IEnumerable和IEnumerator的本质。
下面我们先看IEnumerable和IEnumerator两个接口的语法定义。其实IEnumerable接口是非常的简单,只包含一个抽象的方法GetEnumerator(),它返回一个可用于循环访问集合的IEnumerator对象。IEnumerator对象有什么呢?它是一个真正的集合访问器,没有它,就不能使用foreach语句遍历集合或数组,因为只有IEnumerator对象才能访问集合中的项,假如连集合中的项都访问不了,那么进行集合的循环遍历是不可能的事情了。那么让我们看看IEnumerator接口有定义了什么东西。看下图我们知道IEnumerator接口定义了一个Current属性,MoveNext和Reset两个方法,这是多么的简约。既然IEnumerator对象时一个访问器,那至少应该有一个Current属性,来获取当前集合中的项吧。MoveNext方法只是将游标的内部位置向前移动(就是移到一下个元素而已),要想进行循环遍历,不向前移动一下怎么行呢?
(一)详细讲解:
说到IEnumerable总是会和IEnumerator、foreach联系在一起。C# 支持关键字foreach,允许我们遍历任何数组类型的内容:
int[] myArrayOfInts = {10,20,30,40}; foreach(int i in my myArrayOfInts) { Console.WirteLine(i); }
虽然看上去只有数组才可以使用这个结构,其实任何支持GetEnumerator()方法的类型都可以通过foreach结构进行运算。
public class Garage { Car[] carArray = new Car[4]; //在Garage中定义一个Car类型的数组carArray,其实carArray在这里的本质是一个数组字段 //启动时填充一些Car对象 public Garage() { //为数组字段赋值 carArray[0] = new Car("Rusty", 30); carArray[1] = new Car("Clunker", 50); carArray[2] = new Car("Zippy", 30); carArray[3] = new Car("Fred", 45); } }
理想情况下,与数据值数组一样,使用foreach构造迭代Garage对象中的每一个子项比较方便:
让人沮丧的是,编译器通知我们Garage类没有实现名为GetEnumerator()的方法(显然用foreach遍历Garage对象是不可能的事情,因为Garage类没有实现GetEnumerator()方法,Garage对象就不可能返回一个IEnumerator对象,没有IEnumerator对象,就不可能调用方法MoveNext(),调用不了MoveNext,就不可能循环的了)。这个方法是有隐藏在System.collections命名空间中的IEnumerable接口定义的。(特别注意,其实我们循环遍历的都是对象而不是类,只是这个对象是一个集合对象),支持这种行为的类或结构实际上是宣告它们向调用者公开所包含的子项:
//这个接口告知调方对象的子项可以枚举 public interface IEnumerable { IEnumerator GetEnumerator(); }
可以看到,GetEnumerator方法返回对另一个接口System.Collections.IEnumerator的引用。这个接口提供了基础设施,调用方可以用来移动IEnumerable兼容容器包含的内部对象。
//这个接口允许调用方获取一个容器的子项 public interface IEnumerator { bool MoveNext(); //将游标的内部位置向前移动 object Current{get;} //获取当前的项(只读属性) void Reset(); //将游标重置到第一个成员前面 }
所以,要想Garage类也可以使用foreach遍历其中的项,那我们就要修改Garage类型使之支持这些接口,可以手工实现每一个方法,不过这得花费不少功夫。虽然自己开发GetEnumerator()、MoveNext()、Current和Reset()也没有问题,但有一个更简单的办法。因为System.Array类型和其他许多类型(如List)已经实现了IEnumerable和IEnumerator接口,你可以简单委托请求到System.Array,如下所示:
namespace MyCarIEnumerator { public class Garage:IEnumerable { Car[] carArray = new Car[4]; //启动时填充一些Car对象 public Garage() { carArray[0] = new Car("Rusty", 30); carArray[1] = new Car("Clunker", 50); carArray[2] = new Car("Zippy", 30); carArray[3] = new Car("Fred", 45); } public IEnumerator GetEnumerator() { return this.carArray.GetEnumerator(); } } }
//除此之外,GetEnumerator()被定义为公开的,对象用户可以与IEnumerator类型交互: namespace MyCarIEnumerator { class Program { static void Main(string[] args) { Console.WriteLine("*********Fun with IEnumberable/IEnumerator************\n"); Garage carLot = new Garage(); //交出集合中的每一Car对象吗 foreach (Car c in carLot) //之所以遍历carLot,是因为carLot.GetEnumerator()返回的项时Car类型,这个十分重要 { Console.WriteLine("{0} is going {1} MPH", c.CarName, c.CurrentSpeed); } Console.WriteLine("GetEnumerator被定义为公开的,对象用户可以与IEnumerator类型交互,下面的结果与上面是一致的"); //手动与IEnumerator协作 IEnumerator i = carLot.GetEnumerator(); while (i.MoveNext()) { Car myCar = (Car)i.Current; Console.WriteLine("{0} is going {1} MPH", myCar.CarName, myCar.CurrentSpeed); } Console.ReadLine(); } } }
下面我们来看看手工实现IEnumberable接口和IEnumerator接口中的方法:
namespace ForeachTestCase { //继承IEnumerable接口,其实也可以不继承这个接口,只要类里面含有返回IEnumberator引用的GetEnumerator()方法即可 class ForeachTest:IEnumerable { private string[] elements; //装载字符串的数组 private int ctr = 0; //数组的下标计数器 /// <summary> /// 初始化的字符串 /// </summary> /// <param name="initialStrings"></param> ForeachTest(params string[] initialStrings) { //为字符串分配内存空间 elements = new String[8]; //复制传递给构造方法的字符串 foreach (string s in initialStrings) { elements[ctr++] = s; } } /// <summary> /// 构造函数 /// </summary> /// <param name="source">初始化的字符串</param> /// <param name="delimiters">分隔符,可以是一个或多个字符分隔</param> ForeachTest(string initialStrings, char[] delimiters) { elements = initialStrings.Split(delimiters); } //实现接口中得方法 public IEnumerator GetEnumerator() { return new ForeachTestEnumerator(this); } private class ForeachTestEnumerator : IEnumerator { private int position = -1; private ForeachTest t; public ForeachTestEnumerator(ForeachTest t) { this.t = t; } #region 实现接口 public object Current { get { return t.elements[position]; } } public bool MoveNext() { if (position < t.elements.Length - 1) { position++; return true; } else { return false; } } public void Reset() { position = -1; } #endregion } static void Main(string[] args) { // ForeachTest f = new ForeachTest("This is a sample sentence.", new char[] { ' ', '-' }); ForeachTest f = new ForeachTest("This", "is", "a", "sample", "sentence."); foreach (string item in f) { System.Console.WriteLine(item); } Console.ReadKey(); } } }
(二)IEnumerable<T>接口
实现了IEnmerable<T>接口的集合,是强类型的。它为子对象的迭代提供类型更加安全的方式。
public class ListBoxTest:IEnumerable<String> { private string[] strings; private int ctr = 0; #region IEnumerable<string> 成员 //可枚举的类可以返回枚举 public IEnumerator<string> GetEnumerator() { foreach (string s in strings) { yield return s; } } #endregion #region IEnumerable 成员 //显式实现接口 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion //用字符串初始化列表框 public ListBoxTest(params string[] initialStrings) { //为字符串分配内存空间 strings = new String[8]; //复制传递给构造方法的字符串 foreach (string s in initialStrings) { strings[ctr++] = s; } } //在列表框最后添加一个字符串 public void Add(string theString) { strings[ctr] = theString; ctr++; } //允许数组式的访问 public string this[int index] { get { if (index < 0 || index >= strings.Length) { //处理不良索引 } return strings[index]; } set { strings[index] = value; } } //发布拥有的字符串数 public int GetNumEntries() { return ctr; } }
class Program { static void Main(string[] args) { //创建一个新的列表框并初始化 ListBoxTest lbt = new ListBoxTest("Hello", "World"); //添加新的字符串 lbt.Add("Who"); lbt.Add("Is"); lbt.Add("Douglas"); lbt.Add("Adams"); //测试访问 string subst = "Universe"; lbt[1] = subst; //访问所有的字符串 foreach (string s in lbt) { Console.WriteLine("Value:{0}", s); } Console.ReadKey(); } }
方案1:让这个类实现IEnumerable接口
方案2:这个类有一个public的GetEnumerator的实例方法,并且返回类型中有public 的bool MoveNext()实例方法和public的Current实例属性。
四、foreach和for的区别
(一)、foreach循环的优势
C#支持foreach关键字,foreach在处理集合和数组相对于for存在以下几个优势:
1、foreach语句简洁
2、效率比for要高(C#是强类型检查,for循环对于数组访问的时候,要对索引的有效值进行检查)
3、不用关心数组的起始索引是几(因为有很多开发者是从其他语言转到C#的,有些语言的起始索引可能是1或者是0)
4、处理多维数组(不包括锯齿数组)更加的方便,代码如下:
int[,] nVisited ={ {1,2,3}, {4,5,6}, {7,8,9} }; // Use "for" to loop two-dimension array(使用for循环二维数组) Console.WriteLine("User 'for' to loop two-dimension array"); for (int i = 0; i < nVisited.GetLength(0); i++) for (int j = 0; j < nVisited.GetLength(1); j++) Console.Write(nVisited[i, j]); Console.WriteLine(); //Use "foreach" to loop two-dimension array(使用foreach循环二维数组) Console.WriteLine("User 'foreach' to loop two-dimension array"); foreach (var item in nVisited) Console.Write(item.ToString());
foreach只用一行代码就将所有元素循环了出来,而for循环则就需要很多行代码才可以.
注:foreach处理锯齿数组需进行两次foreach循环
int[][] nVisited = new int[3][]; nVisited[0] = new int[3] { 1, 2, 3 }; nVisited[1] = new int[3] { 4, 5, 6 }; nVisited[2] = new int[6] { 1, 2, 3, 4, 5, 6 }; //Use "foreach" to loop two-dimension array(使用foreach循环二维数组) Console.WriteLine("User 'foreach' to loop two-dimension array"); foreach (var item in nVisited) foreach (var val in item) Console.WriteLine(val.ToString());
5、在类型转换方面foreach不用显示地进行类型转换
int[] val = { 1, 2, 3 }; ArrayList list = new ArrayList(); list.AddRange(val); foreach (int item in list)//在循环语句中指定当前正在循环的元素的类型,不需要进行拆箱转换 { Console.WriteLine((2*item)); } Console.WriteLine(); for (int i = 0; i < list.Count; i++) { int item = (int)list[i];//for循环需要进行拆箱 Console.WriteLine(2 * item); }
6、当集合元素如List<T>等在使用foreach进行循环时,每循环完一个元素,就会释放对应的资源,代码如下:
using (IEnumerator<T> enumerator = collection.GetEnumerator()) { while (enumerator.MoveNext()) { this.Add(enumerator.Current); } }
(二)、foreach循环的劣势
1、上面说了foreach循环的时候会释放使用完的资源,所以会造成额外的gc开销,所以使用的时候,请酌情考虑
2、foreach也称为只读循环,所以再循环数组/集合的时候,无法对数组/集合进行修改。
3、数组中的每一项必须与其他的项类型相等.
五、索引器
实际工作中我们遇到索引器的地方有很多,我们都是似曾相似,今天我们一起来揭开他们的神秘面纱项目开发中在获得DataGridView控件的某列值时:dgvlist.SelectedRows[0].Cells[0].Value;在获得ListView控件的某列值时:listView1.SelectedItems[0].SubItems[0].Text;
在读取数据库记录给变量赋值时:result=dr["StudentName"].ToString();记得Dictionary中根据key值来获取Value值时:dic["key"]等等
(一)、索引器的定义
C#中的类成员可以是任意类型,包括数组和集合。当一个类包含了数组和集合成员时,索引器将大大简化对数组或集合成员的存取操作。
定义索引器的方式与定义属性有些类似,其一般形式如下:
[修饰符] 数据类型 this[索引类型 index] { get{//获得属性的代码} set{ //设置属性的代码} }
(二)、索引器的本质是属性
下面我们以一个例子来了解索引器的用法和原理:
1.创建一个Test类,里面定义一个数组和一个索引器
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 索引器 { public class Test { //01.首先定义一个数组 private string[] name=new string[2]; //02.根据创建索引器的语法定义一个索引器,给name数组赋值和取值 public string this[int index] { get { return name[index]; } set { name[index] = value; } } } }
2.在Main方法中通过索引器给Test类中的数组赋值
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 索引器 { class Program { static void Main(string[] args) { //01.首先你得实例化Test类的对象 Test test=new Test(); //02.利用索引器给Test类中的数组赋值 test[0] = "张总"; test[1] = "吴总"; //03.打印查看效果 for (int i = 0; i < 2; i++) { Console.WriteLine(test[i]); } Console.ReadLine(); } } }
3.效果如下:
上面说到 索引器的本质是属性。是的,我们可以发现上面定义索引器的代码的IL(中间语言)是(属性的格式):
(三)、实际开发例子:
设计一个关于班级的类MyClass,里面有一个list用来记录班级里的所有学生,假设学生类已经定义为Student,那么MyClass的定义大致如下:
public class MyClass { private List<Student> allStudents; //其他字段方法 //.... }
现在想为MyClass定义一个操作,因为学生都是有学号的,所以我想直接用学号来得到某个具体学生的信息,当然了,你可以设计一个public的方法,就像下面一样:
public class MyClass { private List<Student> allStudents; public Student GetStudentByID(int studentID) { if(allStudents==null) return null; for(int i=0;i<allStudents.Count;i++) { if(allStudents[i].id==studentID) { return allStudents[i]; } } return null; } //其他字段方法 //.... }
当然了,使用方法就是:
MyClass mc=new MyClass(); Student st=mc.GetStudentByID(123);
而除了这种添加public函数的方法,索引器也能做到相应的功能,比如我设计一个索引器如下:
public class MyClass { private List<Student> allStudents; public this[int studentID] { get { if(allStudents==null) return null; for(int i=0;i<allStudents.Count;i++) { if(allStudents[i].id==studentID) { return allStudents[i]; } } return null; } } //其他字段方法 //.... }
而使用方法就是:
MyClass mc=new MyClass(); Student st=mc[123];
总结一下,索引器其实就是利用索引(index)找出具体数据的方法(函数),在类的设计中它并不是必须的,它是可以用普通函数替代的,简单点理解,其实就是函数的一种特殊写法而已
(四)、无参属性特征
由于某些不恰当使用字段会破坏对象的状态,所以一般会将所有字段都设为private。要允许用户或类型获取或设置状态信息,需要提供封装了字段访问的方法(访问器)。
public class Employee { private string name; public string GetName(){return name;} public string SetName(string value){name = value;} }
public class Employee { private string name; public string Name { get{return name;} set{name=value;} } }
每个属性都有名称和类型(类型不能是void)。属性不能重载。C#编译器发现获取或设置属性时,实际会生成如下代码,在属性名之前自动附加get_,set_前缀。
public class Employe { private string name; public string get_Name() { return n_Name; } pubic void set_Name() { n_Name=value; } }
针对源代码中定义的每个属性,编译器会在托管程序集的元数据中生成一个属性定义项,包含一些标志以及属性类型。同时还引用了get和set访问器方法。这些工作将属性这种抽象概念与它的访问器方法之间建立起一个联系。
(五)、自动实现属性
如果仅为了封装一个支持字段而创建属性,C#提供了称为自动属性的简洁语法。public string Name{get;set;}C#会在内部自动声明一个私有字段。
优点:访问该属性的任何代码实际都会调用get和set方法。如果以后决定自己实现get和set方法,则访问属性的任何代码都不必重新编译。然而将Name声明为字段,以后想改为属性,那么访问字段的所有代码都必须重新编译才能访问属性方法。
缺点:AIP的支持字段名称由编译器决定,每次重新编译都可能更改名称。因此任何类型含有一个AIP,就没办法对该类型的实例进行反序列化。而运行时序列化引擎会将字段名持久存储到序列化流中。任何想要序列化或反序列化的类型中都不要使用AIP功能。另外使用AIP不能加断点,手动实现的属性则是可以,方便调试。使用AIP属性必然是可读可写的,只写字段不能读取值有什么用?只读肯定是有默认值,所以AIP作用于整个属性的,不能显式实现一个访问器方法,而让另一个自动实现。
(六)、 属性使用时需注意的点
- 如果定义属性,最好同时为它提供get,set访问器方法
- 属性方法可能抛出异常;字段访问永远不会
- 属性不能作为out和ref参数传给方法;字段可以
- 属性方法可能花较长时间执行,可能需要额外内存
- 属性只是简化了语法,不会提升代码的性能,最好老老实实实现GetXXX和SetXXX方法。
(七)、 有参属性
C#使用数组风格的语法公开有参属性(索引器),可将索引器是C#开发人员对[]操作符的重载。比如定义一个类Student
public class Student { public string this[int n] { get { return Name; } set { Name = n > 0 ? "大于0" : "小于0"; } } private string Name; }
当执行Student stu = new Student();stu[4] = "NNN";时执行set方法,将Name设为大于0;执行stu[4]时输出大于0字样。
(八) 调用属性访问器方法时的性能
对于简单的get和set访问器方法,JIT编译器会将代码内联(将方法也就是访问器方法的代码直接编译到调用它的方法中),避免在运行时发出调用所产生的开销,代价是编译好的方法变得更大。注意:JIT在调试代码时不会内联属性方法,因为这将难以调试,发行版本会使用内联,性能会快。
既然属性本质上是方法,而C#和CLR支持泛型方法,但是C#不允许属性引入自己的泛型类型参数,最主要原因属性本来应该表示可供查询或设置某个对象特征。一旦引入泛型类型参数就意味可能改变查询/设置行为,但属性不应该和行为沾边,公开对象的行为无论是不是泛型,都应该定义方法而非属性。
(九) 属性与索引器的区别
参考资料:《c#高级编程》