C#入门经典(第7版):C# 6.0 & Visual Studio 2015(.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

5.2 复杂的变量类型

除了这些简单的变量类型外,C#还提供了3个较复杂(但非常有用)的变量:枚举、结构和数组。

5.2.1 枚举

本书迄今介绍的每种类型(除string外)都有明确的取值范围。诚然,有些类型(如double)的取值范围非常大,可以看成是连续的,但它们仍是一个固定集合。最简单的示例是bool类型,它只能取两个值:true或false。

有时希望变量取的是一个固定集合中的值。例如,让orientation类型可以存储north、south、east或west中的一个值。

此时可以使用枚举类型。枚举可以完成这个orientation类型的任务:它们允许定义一个类型,其取值范围是用户提供的值的有限集合。所以,需要创建自己的枚举类型orientation,它可以从上述4个值中取一个值。

注意有一个附加步骤—— 不是仅声明一个给定类型的变量,而是声明和描述一个用户定义的类型,再声明这个新类型的变量。

定义枚举

可以用enum关键字定义枚举,如下所示:

        enum <typeName>
        {
          <value1>,
          <value2>,
          <value3>,
          ...
          <valueN>
        }

接着声明这个新类型的变量:

        <typeName> <varName>;

并赋值:

        <varName> = <typeName>.<value>;

枚举使用一个基本类型来存储。枚举类型可取的每个值都存储为该基本类型的一个值,默认情况下该类型为int。在枚举声明中添加类型,就可以指定其他基本类型:

        enum <typeName> : <underlyingType>
        {
          <value1>,
          <value2>,
          <value3>,
          ...
          <valueN>
        }

枚举的基本类型可以是byte、sbyte、short、ushort、int、uint、long和ulong。

默认情况下,每个值都会根据定义的顺序(从0开始),被自动赋予对应的基本类型值。这意味着<value1>的值是0, <value2>的值是1, <value3>的值是2,等等。可以重写这个赋值过程:使用=运算符,指定每个枚举的实际值:

        enum <typeName> : <underlyingType>
        {
          <value1> = <actualVal1>,
          <value2> = <actualVal2>,
          <value3> = <actualVal3>,
          ...
          <valueN> = <actualValN>
        }

还可以使用一个值作为另一个枚举的基础值,为多个枚举指定相同的值:

        enum <typeName> : <underlyingType>
        {
          <value1> = <actualVal1>,
          <value2> = <value1>,
          <value3>,
          ...
          <valueN> = <actualValN>
        }

未赋值的任何值都会自动获得一个初始值,这里使用的值是从比上一个明确声明的值大1开始的序列。例如,在上面的代码中,<value3>的值是<value1> + 1。

注意这可能会产生预料不到的问题,在一个定义(如<value2> = <value1>)后指定的值可能与其他值相同。例如,在下面的代码中,<value4>的值与<value2>的值相同:

        enum <typeName> : <underlyingType>
        {
          <value1> = <actualVal1>,
          <value2>,
          <value3> = <value1>,
          <value4>,
          ...
          <valueN> = <actualValN>
        }

当然,如果这正是希望的结果,代码就是正确的。还要注意,以循环方式赋值可能会产生错误,例如:

        enum <typeName> : <underlyingType>
        {
          <value1> = <value2>,
          <value2> = <value1>
        }

下面看一个示例。其代码定义了一个枚举orientation,然后演示了它的用法。

试一试:使用枚举:Ch05Ex02\Program.cs

(1)在C:\BegVCSharp\Chapter05目录中创建一个新的控制台应用程序Ch05Ex02。

(2)把下列代码添加到Program.cs中:

        namespace Ch05Ex02
        {
          enum orientation : byte
          {
              north = 1,
              south = 2,
            east  = 3,
            west  = 4
          }
          class Program
          {
            static void Main(string[] args)
            {
              orientation myDirection = orientation.north;
              WriteLine($"myDirection = {myDirection}");
              ReadKey();
            }
          }
      }

(3)运行应用程序,应得到如图5-5所示的输出结果。

图5-5

(4)退出应用程序,修改代码,如下所示:

        byte directionByte;
        string directionString;
        orientation myDirection = orientation.north;
        WriteLine($"myDirection = {myDirection}");
        directionByte = (byte)myDirection;
        directionString = Convert.ToString(myDirection);
        WriteLine($"byte equivalent = {directionByte}");
        WriteLine($"string equivalent = {directionString}");
        ReadKey();

(5)再次运行应用程序,输出结果如图5-6所示。

图5-6

示例说明

这段代码定义并使用了一个枚举类型orientation。首先要注意,类型定义代码放在名称空间Ch05Ex02中,但没有与其余代码放在一起。这是因为在运行期间,定义代码并不像执行应用程序中的代码那样一行一行地执行。应用程序是从我们熟悉的位置开始执行的,它可以访问新类型,因为该类型位于同一个名称空间中。

这个示例的第一个迭代演示了创建新类型的变量,给它赋值以及把它输出到屏幕上的基本方法。接着修改代码,把枚举值转换为其他类型。注意这里必须使用显式转换。即使orientation的基本类型是byte,也仍必须使用(byte)强制实现类型转换,把myDirection的值转换为byte类型:

        directionByte = (byte)myDirection;

如果要将byte类型转换为orientation,也同样需要进行显式转换。例如,可以使用下述代码将byte变量myByte转换为orientation值,并将这个值赋给myDirection:

        myDirection = (orientation)myByte;

当然,这里必须小心,因为并不是所有byte类型变量的值都可以映射为已定义的orientation值。orientation类型可以存储其他byte值,所以这么做不会直接产生一个错误,但会在应用程序的后面违反逻辑。

要获得枚举的字符串值,可以使用Convert.ToString():

        directionString = Convert.ToString(myDirection);

使用(string)强制类型转换是行不通的,因为需要进行的处理并不仅是把存储在枚举变量中的数据放在string变量中,而是更复杂一些。另外,可以使用变量本身的ToString()命令。下面的代码与使用Convert.ToString()的效果相同:

        directionString = myDirection.ToString();

也可以把string转换为枚举值,但其语法稍复杂一些。有一个特定命令用于完成此类转换,即Enum.Parse(),其用法如下:

        (enumerationType)Enum.Parse(typeof(enumerationType), enumerationValueString);

这里使用了另一个运算符typeof,它可以得到操作数的类型。对orientation类型使用这个命令,如下所示:

        string myString = "north";
        orientation myDirection = (orientation)Enum.Parse(typeof(orientation), myString);

当然,并非所有字符串值都会映射为一个orientation值。如果传送的一个值不能映射为枚举值中的一个,就会产生错误。与C#中的其他值一样,这些值是区分大小写的,所以如果字符串与一个值相同,但大小写不同(例如,将myString设置为North而不是north),就会产生错误。

5.2.2 结构

下一个要介绍的变量类型是结构(struct, structure的简写)。结构就是由几个数据组成的数据结构,这些数据可能具有不同的类型。根据这个结构,可以定义自己的变量类型。例如,假定要存储从起点开始到某一位置的路径,路径由方向和距离值(英里)组成。为简单起见,假定该方向是指南针上的一点(这样,方向就可以用上一节的orientation枚举来表示),距离值可以用double类型来表示。

通过前面的代码,可用两个不同的变量来表示路径:

        orientation  myDirection;
        double       myDistance;

像这样使用两个变量,是没有错误的,但在一个地方存储这些信息更加简单(在需要多个路径时,就尤为简单)。

定义结构

使用struct关键字定义结构,如下所示:

        struct <typeName>
        {
          <memberDeclarations>
        }

<memberDeclarations>部分包含变量的声明(称为结构的数据成员),其格式与前面的变量声明一样。每个成员的声明都采用如下形式:

        <accessibility> <type> <name>;

要让调用结构的代码访问该结构的数据成员,可以对<accessibility>使用关键字public,例如:

        struct route
        {
          public orientation direction;
          public double      distance;
        }

定义结构类型后,就可以定义该结构类型的变量:

        route myRoute;

还可以通过句点字符访问这个组合变量中的数据成员:

        myRoute.direction = orientation.north;
        myRoute.distance  = 2.5;

把这个类型放在下面的“试一试”示例中,其中使用上一个“试一试”示例中的orientation枚举和上面的route结构。本例在代码中处理这个结构,以便了解结构的工作原理。

试一试:使用结构:Ch05Ex03\Program.cs

(1)在C:\BegVCSharp\Chapter05目录中创建一个新的控制台应用程序Ch05Ex03。

(2)将下列代码添加到Program.cs中:

        namespace Ch05Ex03
        {
          enum orientation: byte
          {
              north = 1,
              south = 2,
              east  = 3,
              west  = 4
          }
          struct route
          {
              public orientation direction;
              public double      distance;
          }
          class Program
          {
            static void Main(string[] args)
            {
              route myRoute;
              int myDirection = -1;
              double myDistance;
              WriteLine("1) North\n2) South\n3) East\n4) West");
              do
              {
                  WriteLine("Select a direction:");
                  myDirection = ToInt32(ReadLine());
              }
              while ((myDirection < 1) || (myDirection > 4));
              WriteLine("Input a distance:");
              myDistance = ToDouble(ReadLine());
              myRoute.direction = (orientation)myDirection;
              myRoute.distance = myDistance;
              WriteLine($"myRoute specifies a direction of {myRoute.direction} " +
                    $"and a distance of {myRoute.distance}");
              ReadKey();
            }
          }
      }

(3)执行代码,输入一个介于1和4之间的数字,以选择一个方向,输入一个距离值,结果如图5-7所示。

图5-7

示例说明

结构和枚举一样,也是在代码的主体之外声明的。在名称空间声明中声明route结构及其使用的orientation枚举:

        enum orientation: byte
        {
          north= 1,
          south= 2,
          east  = 3,
          west  = 4
        }
        struct route
        {
          public orientation direction;
          public double      distance;
        }

代码的主体结构与前面的一些示例代码类似,要求用户输入一些信息,并显示它们。把方向选择放在do循环中,对用户的输入进行有效性检查,拒绝不属于1-4范围的整数输入(选择该范围中的值可以映射到枚举成员,从而方便赋值)。

注意:不能解释为整数的输入会导致一个错误。本章后面会说明其原因和处理方法。

注意,在引用route的成员时,处理它们的方式与处理成员类型相同的变量完全一样。赋值语句如下所示:

                myRoute.direction = (orientation)myDirection;
                myRoute.distance = myDistance;

可直接把输入的值放到myRoute.distance中,而不会有负面效果,如下所示:

                myRoute.distance = ToDouble(ReadLine());

还应进行有效性验证,但这段代码不存在这一步骤。对结构成员的任何访问都以相同的方式处理。<structVar>.<memberVar>形式的表达式可计算<memberVar>类型的变量。

5.2.3 数组

前面的所有类型有一个共同点:它们都只存储一个值(结构中存储一组值)。有时,需要存储许多数据,这样就会带来不便。有时需要同时存储几个类型相同的值,而不想为每个值使用不同的变量。

例如,假定要对所有朋友的姓名执行一些操作。可以使用简单的字符串变量,如下所示:

        string friendName1 = "Todd Anthony";
        string friendName2 = "Kevin Holton";
        string friendName3 = "Shane Laigle";

但这看起来需要做很多工作,特别是需要编写不同的代码来处理每个变量。例如,不能在循环中迭代这个字符串列表。

另一种方式是使用数组。数组是一个变量的索引列表,存储在数组类型的变量中。例如,有一个数组friendNames存储上述3个名字。在方括号中指定索引,即可访问该数组中的各个成员,如下所示:

        friendNames[<index>]

这个索引是一个整数,第一个条目的索引是0,第二个条目的索引是1,依此类推。这样就可以使用循环遍历所有条目,例如:

        int i;
        for (i = 0; i < 3; i++)
        {
          WriteLine($"Name with index of {i}: {friendNames[i]}");
        }

数组有一个基本类型,数组中的各个条目都是这种类型。friendNames数组的基本类型是字符串,因为它要存储string变量。数组的条目通常称为元素。

1.声明数组

以下述方式声明数组:

        <baseType>[] <name>;

其中,<baseType>可以是任何变量类型,包括本章前面介绍的枚举和结构类型。数组必须在访问之前初始化,不能像下面这样访问数组或给数组元素赋值:

        int[] myIntArray;
        myIntArray[10] = 5;

数组的初始化有两种方式。可以字面值形式指定数组的完整内容,也可以指定数组的大小,再使用关键字new初始化所有数组元素。

要使用字面值指定数组,只需提供一个用逗号分隔的元素值列表,该列表放在花括号中,例如:

        int[] myIntArray = { 5, 9, 10, 2, 99 };

其中,myIntArray有5个元素,每个元素都被赋予一个整数值。

另一种方式需要使用下述语法:

        int[] myIntArray = new int[5];

这里使用关键字new显式地初始化数组,用一个常量值定义其大小。这种方式会给所有数组元素赋予同一个默认值,对于数值类型来说,其默认值是0。也可以使用非常量的变量来进行初始化,例如:

        int[] myIntArray = new int[arraySize];

还可以组合使用这两种初始化方式:

        int[] myIntArray = new int[5] { 5, 9, 10, 2, 99 };

使用这种方式,数组大小必须与元素个数相匹配。例如,不能编写如下代码:

        int[] myIntArray = new int[10] { 5, 9, 10, 2, 99 };

其中数组定义为有10个元素,但只定义了5个元素,所以编译会失败。如果使用变量定义其大小,该变量必须是一个常量,例如:

        const int arraySize = 5;
        int[] myIntArray = new int[arraySize] { 5, 9, 10, 2, 99 };

如果省略了关键字const,运行这段代码就会失败。

与其他变量类型一样,并非必须在声明数组的代码行中初始化该数组。下面的代码是合法的:

        int[] myIntArray;
        myIntArray = new int[5];

下面的“试一试”示例利用了本节开头的示例,创建并使用一个字符串数组。

试一试:使用数组:Ch05Ex04\Program.cs

(1)在C:\BegVCSharp\Chapter05目录中创建一个新的控制台应用程序Ch05Ex04。

(2)将下列代码添加到Program.cs中:

        static void Main(string[] args)
        {
          string[] friendNames = { "Todd Anthony", "Kevin Holton",
                                    "Shane Laigle" };
          int i;
          WriteLine($"Here are {friendNames.Length} of my friends:");
          for (i = 0; i < friendNames.Length; i++)
          {
              WriteLine(friendNames[i]);
          }
          ReadKey();
        }

(3)执行代码,结果如图5-8所示。

图5-8

示例说明

这段代码用3个值建立了一个string数组,并在for循环中把它们列在控制台上。使用friendNames.Length来确定数组中的元素个数:

        WriteLine($"Here are {friendNames.Length} of my friends:");

这是获取数组大小的简便方法。在for循环中输出值容易出错。例如,把<改为<=,如下所示:

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

编译并执行上述代码,就会弹出如图5-9所示的对话框。

图5-9

这里,代码试图访问friendNames[3]。记住,数组索引从0开始,所以最后一个元素是friendNames[2]。如果试图访问超出数组大小的元素,代码就会出问题。还可以通过一个更具弹性的方法来访问数组的所有成员,即使用foreach循环。

2. foreach循环

foreach循环可以使用一种简便的语法来定位数组中的每个元素:

        foreach (<baseType> <name> in <array>)
        {
          // can use <name> for each element
        }

这个循环会迭代每个元素,依次把每个元素放在变量<name>中,且不存在访问非法元素的危险。不需要考虑数组中有多少个元素,并可以确保将在循环中使用每个元素。使用这个循环,可以修改上个示例中的代码,如下所示:

        static void Main(string[] args)
        {
          string[] friendNames = { "Todd Anthony", "Kevin Holton",
                                    "Shane Laigle" };
          WriteLine($"Here are {friendNames.Length} of my friends:");
          foreach (string friendName in friendNames)
          {
              WriteLine(friendName);
          }
          ReadKey();
        }

这段代码的输出结果与前面的“试一试”示例完全相同。使用这种方法和标准的for循环的主要区别在于:foreach循环对数组内容进行只读访问,所以不能改变任何元素的值。例如,不能编写如下代码:

        foreach (string friendName in friendNames)
        {
          friendName = "Rupert the bear";
        }

如果编译这段代码,就会失败。但如果使用简单的for循环,就可以给数组元素赋值。

3.多维数组

多维数组是使用多个索引访问其元素的数组。例如,假定要确定一座山相对于某位置的高度,可使用两个坐标x和y来指定一个位置。把这两个坐标用作索引,让数组hillHeight可以用每对坐标来存储高度,这就要使用多维数组了。

像这样的二维数组可以声明如下:

        <baseType>[, ] <name>;

多维数组只需要更多逗号,例如:

        <baseType>[, , , ] <name>;

该语句声明了一个4维数组。赋值也使用类似的语法,用逗号分隔大小。要声明和初始化二维数组hillHeight,其基本类型是double, x的大小是3, y的大小是4,则需要:

        double[, ] hillHeight = new double[3,4];

还可以使用字面值进行初始赋值。这里使用嵌套的花括号块,它们之间用逗号分开,例如:

        double[, ] hillHeight = { { 1, 2, 3, 4 }, { 2, 3, 4, 5 }, { 3, 4, 5, 6 } };

这个数组的维度与前面的相同,也是3行4列。通过提供字面值隐式定义了这些维度。

要访问多维数组中的每个元素,只需要指定它们的索引,并用逗号分开,例如:

        hillHeight[2,1]

接着就可以像其他元素那样处理它了。这个表达式将访问上面定义的第3个嵌套数组中的第2个元素(其值是4)。记住,索引从0开始,第一个数字是嵌套的数组。换言之,第一个数字指定花括号对,第2个数字指定该对花括号中的元素。用图5-10来表示这个数组。

图5-10

foreach循环可以访问多维数组中的所有元素,其方式与访问一维数组相同,例如:

        double[, ] hillHeight = { { 1, 2, 3, 4 }, { 2, 3, 4, 5 }, { 3, 4, 5, 6 } };
        foreach (double height in hillHeight)
        {
          WriteLine("{0}", height);
        }

元素的输出顺序与赋予字面值的顺序相同(这里显示了元素的标识符而非实际值):

        hillHeight[0,0]
        hillHeight[0,1]
        hillHeight[0,2]
        hillHeight[0,3]
        hillHeight[1,0]
        hillHeight[1,1]
        hillHeight[1,2]
        ...

4.数组的数组

上一节讨论的多维数组可称为矩形数组,这是因为每一行的元素个数都相同。使用上一个示例,任何一个x坐标都可以对应0至3的y坐标。

也可以使用锯齿数组(jagged array),其中每行的元素个数可能不同。为此,需要有这样一个数组,其中的每个元素都是另一个数组。也可以有数组的数组的数组,甚至更复杂的数组。但是,注意这些数组都必须有相同的基本类型。

声明数组的数组时,其语法要求在数组的声明中指定多个方括号对,例如:

        int[][] jaggedIntArray;

但初始化这样的数组不像初始化多维数组那样简单,例如不能采用以下声明方式:

        jaggedIntArray = new int[3][4];

即使这样做了,也不是很有效,因为使用简单的多维数组可以较为轻松地取得相同的结果。也不能使用下面的代码:

        jaggedIntArray = { { 1, 2, 3 }, { 1 }, { 1, 2 } };

有两种方式:可以初始化包含其他数组的数组(为清晰起见,称其为子数组),然后依次初始化子数组。

        jaggedIntArray = new int[2][];
        jaggedIntArray[0] = new int[3];
        jaggedIntArray[1] = new int[4];

也可以使用上述字面值赋值的一种改进形式:

        jaggedIntArray = new int[3][] { new int[] { 1, 2, 3 }, new int[] { 1 },
                                        new int[] { 1, 2 } };

也可以进行简化,把数组的初始化和声明放在同一行上,如下所示:

        int[][] jaggedIntArray = { new int[] { 1, 2, 3 }, new int[] { 1 },
                                    new int[] { 1, 2 } };

可以对锯齿数组使用foreach循环,但通常需要使用嵌套的foreach循环才能得到实际数据。例如,假定下述锯齿数组包含10个数组,每个数组又包含一个整数数组,其元素是1-10的约数:

        int[][] divisors1To10 = { new int[] { 1 },
                                  new int[] { 1, 2 },
                                  new int[] { 1, 3 },
                                  new int[] { 1, 2, 4 },
                                  new int[] { 1, 5 },
                                  new int[] { 1, 2, 3, 6 },
                                  new int[] { 1, 7 },
                                  new int[] { 1, 2, 4, 8 },
                                  new int[] { 1, 3, 9 },
                                  new int[] { 1, 2, 5, 10 } };

下面的代码会失败:

        foreach (int divisor in divisors1To10)
        {
          WriteLine(divisor);
        }

这是因为数组divisors1To10包含int[ ]元素而不是int元素。正确的做法是循环遍历每个子数组和数组本身:

        foreach (int[] divisorsOfInt in divisors1To10)
        {
          foreach(int divisor in divisorsOfInt)
          {
              WriteLine(divisor);
          }
        }

可以看出,使用锯齿数组的语法要复杂得多!大多数情况下,使用矩形数组比较简单,这是一种比较简单的存储方式。但是,有时必须使用锯齿数组,所以知道怎么使用它们是没有坏处的。一个例子是,使用XML文档,其中一些元素有子元素,而一些元素没有。