C--2012-说明指南-四-

56 阅读1小时+

C# 2012 说明指南(四)

原文:Illustrated C# 2012

协议:CC BY-NC-SA 4.0

十一、枚举

枚举

枚举是程序员定义的类型,如类或结构。

像结构一样,枚举是值类型,因此直接存储它们的数据,而不是用引用和数据分开存储。* Enumeration has only one member type: named constants with integer values.

下面的代码展示了一个名为TrafficLight的新枚举类型的声明示例,它包含三个成员。请注意,成员声明列表是一个逗号分隔的列表;枚举声明中没有分号。

  Keyword      Enum name      ↓        ↓     enum TrafficLight     {        Green,   ←  Comma separated—no semicolons        Yellow,  ←  Comma separated—no semicolons        Red     }

每个枚举类型都有一个底层整数类型,默认情况下是int

  • Each enumeration member is assigned a constant value of the underlying type.
  • By default, the compiler assigns 0 to the first member, and gives each subsequent member a value greater than the value of the previous member by 1.

例如,在TrafficLight类型中,编译器将int012分别赋给成员GreenYellowRed。在下面代码的输出中,您可以通过将底层成员值转换为类型int来查看它们。图 11-1 显示了它们在堆栈上的排列。

`   TrafficLight t1 = TrafficLight.Green;    TrafficLight t2 = TrafficLight.Yellow;    TrafficLight t3 = TrafficLight.Red;

   Console.WriteLine("{0},\t{1}",   t1, (int) t1);    Console.WriteLine("{0},\t{1}",   t2, (int) t2);    Console.WriteLine("{0},\t{1}\n", t3, (int) t3);                                           ↑                                        Cast to int`

这段代码产生以下输出:


Green,  0 Yellow, 1 Red,    2


Image

图 11-1 。枚举的成员常数由基础整数值表示。

可以将枚举值赋给枚举类型的变量。例如,下面的代码显示了三个类型为TrafficLight的变量的声明。请注意,您可以将成员文字赋给变量,也可以从同类型的另一个变量中复制值。

`class Program    {       static void Main()       {               Type   Variable     Member                ↓       ↓           ↓                 TrafficLight t1 = TrafficLight.Red;        // Assign from member          TrafficLight t2 = TrafficLight.Green;      // Assign from member          TrafficLight t3 = t2;                      // Assign from variable

         Console.WriteLine(t1);          Console.WriteLine(t2);          Console.WriteLine(t3);       }    }`

这段代码产生以下输出。请注意,成员名称被打印为字符串。


Red Green Green


设置底层类型和显式值

通过在枚举名后面加上冒号和类型名,可以使用除了int之外的整数类型。该类型可以是任何整数类型。所有成员常量都是枚举的基础类型。

                   Colon                      ↓    enum TrafficLight : ulong    {                     ↑       ...           Underlying type

成员常量的值可以是基础类型的任何值。若要显式设置成员的值,请在枚举声明中在其名称后使用初始值设定项。可以有重复的值,但不能有重复的名称,如下所示:

   enum TrafficLight    {       Green  = 10,       Yellow = 15,                 // Duplicate values       Red    = 15                  // Duplicate values    }

例如,图 11-2 中的代码显示了枚举TrafficLight的两个等价声明。

  • The code on the left accepts the default type and number.
  • The code on the right explicitly sets the underlying type to int and sets the member to the value corresponding to the default value.

Image

图 11-2 。等效枚举声明

隐含成员编号

您可以显式地为任何成员常量赋值。如果不初始化成员常量,编译器会隐式地给它赋值。图 11-3 展示了编译器用来分配这些值的规则。

  • The values associated with member names need not be different.

Image

图 11-3 。分配成员值的算法

例如,下面的代码声明了两个枚举。CardSuit接受成员的隐式编号,如注释中所示。FaceCards显式设置一些成员,并接受其他成员的隐式编号。

`   enum CardSuit    {        Hearts,                   // 0  - Since this is first        Clubs,                    // 1  - One more than the previous one        Diamonds,                 // 2  - One more than the previous one        Spades,                   // 3  - One more than the previous one        MaxSuits                  // 4  - A common way to assign a constant    }                             //      to the number of listed items

   enum FaceCards    {        // Member                 // Value assigned        Jack              = 11,   // 11 - Explicitly set        Queen,                    // 12 - One more than the previous one        King,                     // 13 - One more than the previous one        Ace,                      // 14 - One more than the previous one        NumberOfFaceCards = 4,    // 4  - Explicitly set        SomeOtherValue,           // 5  - One more than the previous one        HighestFaceCard   = Ace   // 14 - Ace is defined above    }`

位标志

程序员长期以来使用单个字中的不同位作为表示一组开/关标志的简洁方式。在本节中,我将把这个单词称为标志单词。枚举提供了实现这一点的便捷方式。

一般步骤如下:

  1. Determine how many bit flags you need, and choose an unsigned integer type with enough bits to save them.
  2. Determine what each bit represents and name it. Declares an enumeration of the selected integer type, each member being represented by a bit position.
  3. Use the bitwise OR operator to set the appropriate bit in the word that holds the bit flag.
  4. Then, you can check whether a specific bit flag is set by using the HasFlag method or the bitwise AND operator.

例如,下面的代码显示了代表纸牌游戏中一副纸牌的选项的枚举声明。底层类型uint足以容纳所需的四个位标志。请注意以下关于代码的内容:

  • Members have names that represent binary options.
    • Each option is represented by a specific bit position in the word. The bit position remains 0 or 1.
    • You don't want to use 0 as a member value because the bit flag represents a set of bits that are either on or off. It already has a meaning-all bit flags are turned off.
  • In hexadecimal notation, each hexadecimal digit represents exactly four digits. Because of this direct relationship between bit pattern and hexadecimal representation, when using bit pattern, hexadecimal representation is usually used instead of decimal representation.
  • In fact, it is not necessary to decorate enumeration with Flags attribute, but it provides some extra convenience, which I will discuss soon. Property is displayed as a string in square brackets, just before the language construction. In this case, the attribute immediately precedes the enumeration declaration. I will talk about attributes in Chapter 24.

   [Flags]    enum CardDeckSettings : uint    {       SingleDeck    = 0x01,            // Bit 0       LargePictures = 0x02,            // Bit 1       FancyNumbers  = 0x04,            // Bit 2       Animation     = 0x08             // Bit 3    }

图 11-4 说明了这种枚举。

Image

图 11-4 。标志位的定义(左),以及它们各自的表示(右)

要创建具有适当位标志的单词,请声明一个枚举类型的变量,并使用按位 OR 运算符来设置所需的位。例如,下面的代码设置了标志字中四个选项中的三个:

Enum type     Flag word       Bit flags ORed together           ↓           ↓       <ins>         ↓                 </ins>    CardDeckSettings ops =    CardDeckSettings.SingleDeck                            | CardDeckSettings.FancyNumbers                            | CardDeckSettings.Animation ;

要检查标志字是否设置了特定的位标志,可以使用 enum 类型的 Boolean HasFlag方法。您对标志字调用HasFlag方法,传入您正在检查的位标志,如下面的代码行所示。如果设置了指定的位标志,HasFlag返回true;否则,它返回false

   bool useFancyNumbers = ops.HasFlag(<ins>CardDeckSettings.FancyNumbers</ins>);                            ↑                         ↑                         Flag word                    Bit flag

HasFlag方法也可以检查多个位标志。例如,以下代码检查标志字ops是否同时设置了AnimationFancyNumbers位。该代码执行以下操作:

  • The first statement creates a test word instance called testFlags with Animation and FancyNumbers bits set. It then passes testFlags as a parameter to the HasFlag method.
  • HasFlags Check whether all flags set in the test word are also set in the flag word ops. If yes, then HasFlag returns to true. Otherwise, return to false.

`   CardDeckSettings testFlags =                CardDeckSettings.Animation | CardDeckSettings.FancyNumbers;

   bool useAnimationAndFancyNumbers = ops.HasFlag( testFlags );                                        ↑                ↑                                     Flag word           Test word`

确定是否设置了一个或多个特定位的另一种方法是使用按位 AND 运算符。例如,像上面的代码一样,下面的代码检查一个标志字,以查看是否设置了FancyNumbers位标志。这是通过将标志字与位标志进行“与”运算,然后将结果与位标志进行比较来实现的。如果该位是在原始标志字中设置的,那么 AND 运算的结果将具有与位标志相同的位模式。

   bool useFancyNumbers =         (ops & <ins>CardDeckSettings.FancyNumbers</ins>) == CardDeckSettings.FancyNumbers;           ↑                   ↑        Flag word           Bit flag

图 11-5 说明了创建标志字,然后使用按位 and 运算来确定特定位是否被置位的过程。

Image

图 11-5 。产生一个标志字并检查它是否有特定的位标志

旗帜属性

前面的代码在声明枚举之前使用了Flags属性,如下所示:

   [Flags]    enum CardDeckSettings : uint    {       ...    }

Flags属性根本不会改变计算。然而,它确实提供了几个方便的特性。首先,它通知编译器、对象浏览器和其他查看代码的工具,枚举的成员应该作为位标志组合在一起,而不是仅作为单独的值使用。这允许浏览器更恰当地解释枚举类型的变量。

其次,它允许枚举的ToString方法为位标志的值提供更合适的格式。ToString方法获取一个枚举值,并将其与该枚举的常量成员的值进行比较。如果匹配其中一个成员,ToString返回该成员的字符串名称。

例如,检查下面的代码,其中枚举前面没有Flags属性。

`   enum CardDeckSettings : uint    {       SingleDeck     = 0x01,        // bit 0       LargePictures  = 0x02,        // bit 1       FancyNumbers   = 0x04,        // bit 2       Animation      = 0x08         // bit 3    }

   class Program    {       static void Main( )       {          CardDeckSettings ops;

         ops = CardDeckSettings.FancyNumbers;                   // Set one flag.          Console.WriteLine( ops.ToString() );                                                                 // Set two bit flags.          ops = CardDeckSettings.FancyNumbers | CardDeckSettings.Animation;          Console.WriteLine( ops.ToString() );                   // Print what?       }    }`

该代码产生以下输出:


FancyNumbers 12


在这段代码中,方法Main执行以下操作:

  • Create a variable of enumeration type CardDeckSettings, set a bit flag of it, and print out the value of the variable, namely the value FancyNumbers
  • Give the variable a new value, which consists of two set bit flags, and print out its value-12.

作为第二次赋值的结果显示的值12是作为intops的值,因为FancyNumbers为值4设置位,而Animation为值8设置位,从而给出值12int。在赋值后的WriteLine方法中,当ToString方法试图查找值为12的枚举成员的名称时,它发现不存在具有该值的成员——所以它只是打印出该值。

然而,如果我们要在声明枚举之前添加回Flags属性,这将告诉ToString方法可以单独考虑这些位。在查找该值时,ToString会发现12对应于两个单独的位标志成员——FancyNumbersAnimation——并会返回包含他们名字的字符串,用逗号和空格分隔。下面显示了使用Flags属性再次运行代码的结果:


FancyNumbers FancyNumbers, Animation


使用位标志的例子

以下代码将使用位标志的所有部分放在一起:

`[Flags]    enum CardDeckSettings : uint    {       SingleDeck     = 0x01,        // bit 0       LargePictures  = 0x02,        // bit 1       FancyNumbers   = 0x04,        // bit 2       Animation      = 0x08         // bit 3    }

   class MyClass    {       bool UseSingleDeck               = false,            UseBigPics                  = false,            UseFancyNumbers             = false,            UseAnimation                = false,            UseAnimationAndFancyNumbers = false;

      public void SetOptions( CardDeckSettings ops )       {          UseSingleDeck     = ops.HasFlag( CardDeckSettings.SingleDeck );          UseBigPics        = ops.HasFlag( CardDeckSettings.LargePictures );          UseFancyNumbers   = ops.HasFlag( CardDeckSettings.FancyNumbers );          UseAnimation      = ops.HasFlag( CardDeckSettings.Animation );

         CardDeckSettings testFlags =                      CardDeckSettings.Animation | CardDeckSettings.FancyNumbers;          UseAnimationAndFancyNumbers = ops.HasFlag( testFlags );       }

      public void PrintOptions( )       {          Console.WriteLine( "Option settings:" );          Console.WriteLine( "  Use Single Deck                 - {0}", UseSingleDeck );          Console.WriteLine( "  Use Large Pictures              - {0}", UseBigPics );          Console.WriteLine( "  Use Fancy Numbers               - {0}", UseFancyNumbers );          Console.WriteLine( "  Show Animation                  - {0}", UseAnimation );          Console.WriteLine( "  Show Animation and FancyNumbers - {0}",                                                            UseAnimationAndFancyNumbers );       }    }                                                                                          Image    class Program    {       static void Main( )       {          MyClass mc = new MyClass( );          CardDeckSettings ops = CardDeckSettings.SingleDeck                                 | CardDeckSettings.FancyNumbers                                 | CardDeckSettings.Animation;          mc.SetOptions( ops );          mc.PrintOptions( );       }    }`

该代码产生以下输出:


Option settings:   Use Single Deck                 - True   Use Large Pictures              - False   Use Fancy Numbers               - True   Show Animation                  - True   Show Animation and FancyNumbers - True


更多关于枚举

枚举只有一种成员类型:声明的成员常量。

  • You cannot use modifiers on members. They all implicitly have the same accessibility as enumeration.
  • Members are static, as you will remember, which means that even if there are no variables of enumerated type, they are accessible. As with all statics, use the type name followed by a dot and a member name to use members.

例如,下面的代码没有创建任何 enum TrafficLight类型的变量,但是因为成员是静态的,所以可以使用WriteLine访问和打印它们。

   static void Main()    {       Console.WriteLine("{0}", TrafficLight.Green);       Console.WriteLine("{0}", TrafficLight.Yellow);       Console.WriteLine("{0}", TrafficLight.Red);    }                                      ↑        ↑                                 Enum name   Member name

枚举是一种独特的类型。比较不同枚举类型的枚举成员会导致编译时错误。例如,下面的代码声明了两个不同的枚举类型,它们具有完全相同的结构和成员名称。

  • The first if statement is good because it compares different members from the same enumeration type.
  • The second if statement generates an error because it attempts to compare members from different enumeration types. This error occurs even if the structure and member names are identical.

`enum FirstEnum                        // First enum type    {       Mem1,       Mem2    }

   enum SecondEnum                        // Second enum type    {       Mem1,       Mem2    }    class Program    {       static void Main()       {          if (FirstEnum.Mem1 < FirstEnum.Mem2)  // OK--members of same enum type             Console.WriteLine("True");

         if (FirstEnum.Mem1 < SecondEnum.Mem1) // Error--different enum types             Console.WriteLine("True");       }    }`

还有几个有用的方法 .NET Enum类型,enum基于该类型:

  • GetName The method accepts an enumeration type object and an integer, and returns the name of the corresponding enumeration member.
  • GetNames The method accepts an enumeration type object and returns the names of all members in the enumeration.

下面的代码显示了正在使用的每种方法的示例。注意,您必须使用typeof操作符来获取枚举类型对象。

`   enum TrafficLight    {       Green,       Yellow,       Red    }

   class Program    {       static void Main()       {          Console.WriteLine( "Second member of TrafficLight is {0}\n",                               Enum.GetName( typeof( TrafficLight ), 1 ) );

         foreach ( var name in Enum.GetNames( typeof( TrafficLight ) ) )             Console.WriteLine( name );       }    }`

该代码产生以下输出:


`Second member of TrafficLight is Yellow

Green Yellow Red`


十二、数组

数组

数组是由单个变量名表示的一组统一的数据元素。使用变量名以及方括号中的一个或多个索引来访问各个元素,如下所示:

   Array name   Index                 ↓    ↓     **MyArray[4]**

定义

让我们从 C# 中与数组有关的一些重要定义开始。

  • 元素:一个数组的各个数据项称为元素。数组的所有元素必须属于同一类型或从同一类型派生。
  • 秩/维数:数组可以有任意正的维数。一个数组的维数叫做它的
  • 维度长度:一个数组的每一个维度都有一个长度,是该方向的位置数。
  • 数组长度:在所有维度中,一个数组包含的元素总数,称为数组的长度
重要细节

以下是一些关于 C# 数组的重要事实:

  • 一旦创建了一个数组,它的大小就固定了。C# 不支持动态数组。
  • 数组索引是从 0 开始的。也就是说,如果一个维度的长度是 n ,那么索引值的范围是从 0 到n–1。例如,图 12-1 显示了两个示例数组的尺寸和长度。请注意,对于每个维度,索引范围从 0 到长度–1。

Image

图 12-1 。尺寸和大小

数组的类型

C# 提供了两种数组:

  • 一维数组可以被认为是一行元素,或者是元素的向量。
  • 多维数组的组成使得主向量中的每个位置都是一个数组,称为子数组。子阵列向量中的位置本身可以是子阵列。

此外,还有两种类型的多维数组:矩形数组交错数组;它们具有以下特征:

  • 矩形阵列
    • 是多维数组,其中特定维中的所有子数组长度相同
    • 无论尺寸有多少,始终使用一组方括号int x = myArray2[4, 6, 1]        // One set of square brackets
  • 交错阵列
    • 是多维数组,其中每个子数组都是独立的数组
    • 可以具有不同长度的子阵列
    • 对数组的每个维度使用一组单独的方括号

jagArray1[2][7][4]                  // Three sets of square brackets

图 12-2 显示了 C# 中可用的数组类型。

Image

图 12-2 。一维、矩形和锯齿状数组

一个数组作为一个对象

数组实例是一个类型从类System.Array派生的对象。因为数组是从这个 BCL 基类派生的,所以它们从这个基类继承了许多有用的成员,例如:

  • Rank:返回数组维数的属性
  • Length:返回数组长度(元素总数)的属性

数组是引用类型,和所有引用类型一样,它们既有对数据的引用,也有对数据对象本身的引用。引用要么在栈上,要么在堆中,而数据对象本身总是在堆中。图 12-3 显示了一个数组的内存配置和组件。

Image

图 12-3 。数组的结构

尽管数组总是引用类型,但数组的元素可以是值类型,也可以是引用类型。

  • 如果存储的元素是值类型,则数组称为值类型数组
  • 如果存储在数组中的元素是引用类型对象的引用,则该数组称为引用类型数组

图 12-4 显示了一个值类型数组和一个引用类型数组。

Image

图 12-4 。元素可以是值或引用。

一维矩形数组

从语法上来说,一维数组和矩形数组非常相似,所以我将它们放在一起处理。然后,我将分别处理交错数组。

声明一维或矩形数组

若要声明一维或矩形数组,请在类型和变量名之间使用一组方括号。

等级说明符是括号之间的逗号。它们指定数组将具有的维数。排名是逗号的数量加一。例如,没有逗号表示一维数组,一个逗号表示二维数组,依此类推。

基本类型和等级说明符是数组的类型。例如,下面一行代码声明了一个一维数组long s。数组的类型是long[],读作“一个长整型数组”。

      Rank specifiers = 1                ↓   <ins>long[ ]</ins> secondArray;           ↑ Array type

下面的代码显示了矩形数组声明的示例。请注意以下事项:

  • 您可以根据需要拥有任意多个等级说明符。
  • 不能在数组类型部分中放置数组维数长度。秩是数组类型的一部分,但是维度的长度是类型的一部分。
  • 当声明一个数组时,维数的是固定的。然而,维度的长度直到数组被实例化才被确定。

`      Rank specifiers                   int[,,]   firstArray;                     // Array type: 3D array of int    int[,]    arr1;                           // Array type: 2D array of int    long[,,]  arr3;                           // Array type: 3D array of long             ↑       Array type

   long[3,2,6] SecondArray;                  // Wrong!  Compile error                 ↑ ↑ ↑    Dimension lengths not allowed!`

Image 注意与 C/C++不同,在 C# 中,括号跟在基类型后面,而不是变量名后面。

实例化一维或矩形数组

要实例化一个数组,可以使用一个数组创建表达式。数组创建表达式由new操作符、基本类型和一对方括号组成。每个维度的长度放在括号之间的逗号分隔列表中。

以下是一维数组声明的示例。

  • 数组arr2是由四个int组成的一维数组。
  • 数组mcArr是四个MyClass引用的一维数组。

图 12-5 显示了它们在内存中的布局。

                                                      Four elements                                                                ↓    int[]     arr2  = new int[4];    MyClass[] mcArr = <ins>new MyClass[4]</ins>;                                                            ↑                        Array-creation expression

下面是一个矩形阵列的示例。

  • 数组arr3是一个三维数组。
  • 数组的长度是 3 * 6 * 2 = 36。

图 12-5 显示了它在内存中的布局。

                                          Lengths of the dimensions                                                         <ins>    ↓    </ins>    int[,,] arr3 = new int[3,6,2] ; Image

图 12-5 。声明和实例化数组

Image 注意与对象创建表达式不同,数组创建表达式不包含括号——即使对于引用类型的数组也是如此。

访问数组元素

使用整数值作为数组的索引来访问数组元素。

  • 每个维度都使用从 0 开始的索引。
  • 索引放在数组名称后面的方括号中。

下面的代码显示了声明、写入和读取一维和二维数组的示例:

`   int[]  intArr1 = new int[15];        // Declare 1D array of 15 elements.    intArr1[2]     = 10;                 // Write to element 2 of the array.    int var1       = intArr1[2];         // Read from element 2 of the array.

   int[,] intArr2 = new int[5,10];      // Declare 2D array.    intArr2[2,3]   = 7;                  // Write to the array.    int var2       = intArr2[2,3];       // Read from the array.`

下面的代码显示了创建和访问一维数组的完整过程:

`   int[] myIntArray;                            // Declare the array.

   myIntArray = new int[4];                     // Instantiate the array.

   for( int i=0; i<4; i++ )                     // Set the values.       myIntArray[i] = i*10;

   // Read and display the values of each element.    for( int i=0; i<4; i++ )         Console.WriteLine("Value of element {0} = {1}", i, myIntArray[i]);`

该代码产生以下输出:


Value of element 0 is 0 Value of element 1 is 10 Value of element 2 is 20 Value of element 3 is 30


初始化一个数组

每当创建数组时,每个元素都会自动初始化为该类型的默认值。预定义类型的默认值为:整数类型为0,浮点类型为0.0,布尔类型为false,引用类型为null

例如,下面的代码创建一个数组,并将其四个元素初始化为值0。图 12-6 说明了内存中的布局。

   int[] intArr = new int[4]; Image

图 12-6 。一维数组的自动初始化

一维数组的显式初始化

对于一维数组,您可以通过在数组实例化的数组创建表达式后立即包含一个初始化列表来设置显式初始值。

  • 初始化值必须用逗号分隔,并用一组花括号括起来。
  • 维度长度是可选的,因为编译器可以根据初始化值的数量来推断长度。
  • 请注意,数组创建表达式和初始化列表之间没有任何分隔。也就是说,没有等号或其他连接运算符。

例如,下面的代码创建一个数组,并将其四个元素初始化为花括号之间的值。图 12-7 显示了内存中的布局。

                                                                  Initialization list                                                               <ins>              ↓              </ins>    int[] intArr = new int[] { 10, 20, 30, 40 };                                                            ↑                          No connecting operator Image

图 12-7 。一维数组的显式初始化

矩形数组的显式初始化

要显式初始化矩形数组,需要遵循以下规则:

  • 初始值的每个向量必须用花括号括起来。
  • 每个维度也必须嵌套在花括号中。
  • 除了初始值,每个维度的初始化列表和组件也必须用逗号分隔。

例如,下面的代码显示了带有初始化列表的二维数组的声明。图 12-8 显示了内存中的布局。

                                                                          Initialization lists separated by commas                                                                                           ↓                ↓    int[,] intArray2 = new int[,] { {10, 1}, {2, 10}, {11, 9} } ; Image

***图 12-8。*初始化矩形阵列

初始化矩形数组的语法点

矩形数组是用嵌套的逗号分隔的初始化列表初始化的。初始化列表嵌套在花括号中。这有时会令人困惑,因此要正确使用嵌套、分组和逗号,请考虑以下提示:

  • 逗号被用作所有元素之间的分隔符
  • 逗号从不放在左花括号之间。
  • 逗号从不放在右花括号之前。
  • 如果可能的话,使用缩进和回车来排列这些组,这样它们在视觉上是不同的。
  • 从左到右阅读等级规范,将最后一个数字指定为“元素”,将所有其他数字指定为“组”

例如,将下面的声明读作“intArray有四组三组两个元素。”

                                                                               Initialization lists, nested and separated by commas    int[,,] intArray = new int[4,3,2] {                   ↓                  ↓                    ↓                                        { {8, 6},  {5,  2}, {12, 9} },                                        { {6, 4},  {13, 9}, {18, 4} },                                        { {7, 2},  {1, 13}, {9,  3} },                                        { {4, 6},  {3,  2}, {23, 8} }                                      };

快捷语法

当在单个语句中组合声明、数组创建和初始化时,可以完全省略语法中的数组创建表达式部分,只提供初始化部分。图 12-9 显示了这种快捷语法。

Image

图 12-9 。数组声明、创建和初始化的快捷方式

隐式类型化数组

到目前为止,我们已经在所有数组声明的开头明确指定了数组类型。但是,像其他局部变量一样,局部数组也可以是隐式类型的。这意味着:

  • 初始化数组时,可以让编译器从初始化器的类型中推断出数组的类型。只要所有的初始值设定项都可以隐式转换为单一类型,这是允许的。
  • 就像隐式类型的局部变量一样,使用关键字var代替数组类型。

下面的代码显示了三个数组声明的显式和隐式版本。第一组是一维数组int s .第二组是二维数组int s .第三组是字符串数组。注意,在隐式类型intArr4的声明中,您仍然需要在初始化中包含秩说明符。

           Explicit                           Explicit       <ins>     ↓    </ins>                                    ↓    int [] intArr1 = new int[] { 10, 20, 30, 40 };    var    intArr2 = new    [] { 10, 20, 30, 40 };         ↑                                           ↑     Keyword                                 Inferred    int[,] intArr3 = new int[,] { { 10, 1 }, { 2, 10 }, { 11, 9 } };    var    intArr4 = new    [,] { { 10, 1 }, { 2, 10 }, { 11, 9 } };                                                              ↑                                                    Rank specifier    string[] sArr1 = new string[] { "life", "liberty", "pursuit of happiness" };    var      sArr2 = new       [] { "life", "liberty", "pursuit of happiness" };

把所有的东西放在一起

下面的代码把我们到目前为止看到的所有部分放在了一起。它创建、初始化并使用一个矩形数组。

`   // Declare, create, and initialize an implicitly typed array.    var arr = new int[,] {{0, 1, 2}, {10, 11, 12}};

   // Print the values.    for( int i=0; i<2; i++ )       for( int j=0; j<3; j++ )          Console.WriteLine("Element [{0},{1}] is {2}", i, j, arr[i,j]);`

该代码产生以下输出:


Element [0,0] is 0 Element [0,1] is 1 Element [0,2] is 2 Element [1,0] is 10 Element [1,1] is 11 Element [1,2] is 12


参差阵列

交错数组是数组的数组。与矩形阵列不同,交错阵列的子阵列可以有不同数量的元素。

例如,下面的代码声明了一个二维交错数组。图 12-10 显示了内存中数组的布局。

  • 第一维的长度是 3。
  • 声明可以读作“jagArr是一个由三个int组成的数组。”
  • 注意,图中显示了四个数组对象——一个用于顶层数组,三个用于子数组。

int[][] jagArr = new int[3][];   // Declare and create top-level array.             ...                  // Declare and create subarrays. Image

***图 12-10。*参差阵列是阵列中的阵列。

声明一个交错的数组

交错数组的声明语法要求每个维度都有一组单独的方括号。数组变量声明中的方括号组数决定了数组的秩。

  • 交错数组可以是大于 1 的任意维数。
  • 与矩形数组一样,维数长度不能包含在声明的数组类型部分中。

   Rank specifiers        <ins>      ↓</ins>    int[][]   SomeArr;             // Rank = 2    <ins>int[][][]</ins> OtherArr;            // Rank = 3                ↑               ↑      Array type        Array name

快捷方式实例化

您可以使用数组创建表达式将交错数组声明与第一级数组的创建结合起来,如下面的声明所示。图 12-11 显示了结果。

                                                Three subarrays                                                              ↓    int[][] jagArr = new int[3][]; Image

***图 12-11。*快捷方式一级实例化

在声明语句中,不能实例化超过一级的数组。

                                                      Allowed                                                             ↓    int[][] jagArr = new int[3][4];              // Wrong! Compile error                                                                     ↑                                                             Not allowed

实例化一个交错的数组

与其他类型的数组不同,您无法在一个步骤中完全实例化交错数组。因为交错数组是由独立数组组成的数组,所以每个数组都必须单独创建。实例化完整的交错数组需要以下步骤:

  1. 实例化顶级数组。
  2. 分别实例化每个子数组,将新创建的数组的引用分配给其包含数组的适当元素。

例如,以下代码显示了二维交错数组的声明、实例化和初始化。请注意,在代码中,对每个子数组的引用都被赋给了顶级数组中的一个元素。代码中的步骤 1 至 4 对应于图 12-12 中的编号表示。

`   int[][] Arr = new int[3][];                  // 1. Instantiate top level.

   Arr[0] = new int[] {10, 20, 30};             // 2. Instantiate subarray.    Arr[1] = new int[] {40, 50, 60, 70};         // 3. Instantiate subarray.    Arr[2] = new int[] {80, 90, 100, 110, 120};  // 4. Instantiate subarray.` Image

***图 12-12。*创建二维交错数组

交错排列的子阵列

因为交错数组中的子数组本身就是数组,所以交错数组中可能有矩形数组。例如,下面的代码创建一个由三个二维矩形数组组成的交错数组,并用值初始化它们。然后显示这些值。图 12-13 说明了该结构。

该代码使用从System.Array继承的数组的GetLength(int n)方法来获取数组的指定维度的长度。

`   int[][,] Arr;         // An array of 2D arrays    Arr = new int[3][,];  // Instantiate an array of three 2D arrays.

   Arr[0] = new int[,] { { 10,  20  },                          { 100, 200 } };

   Arr[1] = new int[,] { { 30,  40,  50  },                          { 300, 400, 500 }  };

   Arr[2] = new int[,] { { 60,  70,  80,  90  },                          { 600, 700, 800, 900 } };

                                                                                 ↓ Get length of dimension 0 of Arr.    for (int i = 0; i < Arr.GetLength(0); i++)    {                                                                                               ↓ Get length of dimension 0 of Arr[ i ].       for (int j = 0; j < Arr[i].GetLength(0); j++)       {                                                                                                            ↓ Get length of dimension 1 of Arr[ i ].          for (int k = 0; k < Arr[i].GetLength(1); k++)          {              Console.WriteLine                      ("[{0}][{1},{2}] = {3}", i, j, k, Arr[i][j, k]);          }          Console.WriteLine("");       }       Console.WriteLine("");    }`

这段代码产生以下输出:


`[0][1,0] = 100 [0][1,1] = 200

[1][0,0] = 30 [1][0,1] = 40 [1][0,2] = 50

[1][1,0] = 300 [1][1,1] = 400 [1][1,2] = 500

[2][0,0] = 60 [2][0,1] = 70 [2][0,2] = 80 [2][0,3] = 90

[2][1,0] = 600 [2][1,1] = 700 [2][1,2] = 800 [2][1,3] = 900`


Image

***图 12-13。*交错排列的三个二维数组

比较矩形和锯齿状数组

矩形和锯齿状数组的结构有很大的不同。例如,图 12-14 显示了一个 3 乘 3 的矩形数组的结构,以及一个由三个长度为 3 的一维数组组成的锯齿状数组。

  • 两个数组都包含九个整数,但是正如你所看到的,它们的结构是完全不同的。
  • 矩形数组只有一个数组对象,而交错数组有四个数组对象。

Image

***图 12-14。*比较矩形和锯齿状阵列的结构

一维数组在 CIL 中有特定的指令,允许它们针对性能进行优化。矩形阵列没有这些指令,也没有优化到相同的水平。因此,有时使用一维数组的交错数组(可以优化)比使用矩形数组(不能优化)更有效。

另一方面,对于矩形阵列来说,编程复杂度可以显著降低,因为它可以被视为单个单元,而不是阵列的阵列。

foreach 语句

foreach语句允许你顺序访问数组中的每个元素。它实际上是一个更通用的构造,因为它也适用于其他集合类型——但是在这一节中,我将只讨论它在数组中的使用。第十八章讲述了它与其他收藏类型的使用。

foreach声明的要点如下:

  • 迭代变量是与数组元素类型相同的临时变量。foreach语句使用迭代变量顺序表示数组中的每个元素。
  • foreach语句的语法如下所示,其中
    • Type是数组元素的类型。您可以显式地提供它的类型,或者您可以使用var并让它被编译器隐式地类型化和推断,因为编译器知道数组的类型。
    • Identifier是迭代变量的名称。
    • ArrayName是要迭代的数组的名称。
    • Statement是对数组中的每个元素执行一次的简单语句或块。

`       Explicitly typed iteration variable declaration                                        ↓                   foreach( Type Identifier in ArrayName )       Statement

                    Implicitly typed iteration variable declaration                                       ↓                  foreach( var Identifier in ArrayName )       Statement`

在下面的文本中,我有时会使用隐式类型,有时会使用显式类型,这样您就可以看到所使用的确切类型。但是形式在语义上是等价的。

foreach语句的工作方式如下:

  • 它从数组的第一个元素开始,并将该值赋给迭代变量
  • 然后,它执行语句体。在主体内部,可以使用迭代变量作为数组元素的只读别名。
  • 执行完主体后,foreach语句选择数组中的下一个元素,并重复这个过程。

这样,它在数组中循环,允许您逐个访问每个元素。例如,以下代码显示了一个包含四个整数的一维数组的foreach语句的用法:

  • WriteLine语句是foreach语句的主体,对数组的每个元素执行一次。
  • 第一次循环时,迭代变量item具有数组第一个元素的值。每一次,它都有数组中下一个元素的值。

   int[] arr1 = {10, 11, 12, 13};       Iteration variable declaration                          <ins>       ↓        </ins>                                  Iteration variable use    foreach( int item in arr1 )                          ↓       Console.WriteLine("Item Value: {0}", item);

该代码产生以下输出:


Item Value: 10 Item Value: 11 Item Value: 12 Item Value: 13


迭代变量为只读

因为迭代变量的值是只读的,很明显它不能被改变。但是这对值类型数组和引用类型数组有不同的影响。

对于值类型数组,这意味着当数组的元素由迭代变量表示时,不能改变它。例如,在下面的代码中,试图更改迭代变量中的数据会产生一条编译时错误信息:

`   int[] arr1 = {10, 11, 12, 13};

   foreach( int item in arr1 )       item++;     // Compilation error. Changing variable value is not allowed.`

对于引用类型数组,您仍然不能更改迭代变量,但是迭代变量只保存对数据的引用,而不是数据本身。因此,尽管您不能更改引用,但是您可以通过迭代变量更改数据

下面的代码创建一个由四个MyClass对象组成的数组,并初始化它们。在第一个foreach语句中,每个对象中的数据都发生了变化。在第二个foreach语句中,从对象中读取更改的数据。

`   class MyClass    {       public int MyField = 0;    }

   class Program    {       static void Main()       {          MyClass[] mcArray = new MyClass[4];            // Create array.          for (int i = 0; i < 4; i++)          {             mcArray[i] = new MyClass();                 // Create class objects.             mcArray[i].MyField = i;                     // Set field.          }          foreach (MyClass item in mcArray)             item.MyField += 10;                         // Change the data.

         foreach (MyClass item in mcArray)             Console.WriteLine("{0}", item.MyField);     // Read the changed data.       }    }`

该代码产生以下输出:


10 11 12 13


带有多维数组的 foreach 语句

在多维数组中,按照最右边的索引增加最快的顺序处理元素。当索引从 0 到长度–1 时,左边的下一个索引递增,右边的索引重置为 0。

矩形阵列示例

以下示例显示了用于矩形数组的foreach语句:

`   class Program    {       static void Main()       {          int total = 0;          int[,] arr1 = { {10, 11}, {12, 13} };

         foreach( var element in arr1 )          {             total += element;             Console.WriteLine                       ("Element: {0}, Current Total: {1}", element, total);          }       }    }`

该代码产生以下输出:


Element: 10, Current Total: 10 Element: 11, Current Total: 21 Element: 12, Current Total: 33 Element: 13, Current Total: 46


交错排列的例子

因为交错数组是数组的数组,所以必须为交错数组中的每个维度使用单独的foreach语句。foreach语句必须正确嵌套,以确保每个嵌套数组都得到正确处理。

例如,在下面的代码中,第一个foreach语句循环遍历顶级数组arr1,选择下一个要处理的子数组。内部的foreach语句处理该子数组的元素。

`   class Program    {       static void Main( )       {          int total    = 0;          int[][] arr1 = new int[2][];          arr1[0]      = new int[] { 10, 11 };          arr1[1]      = new int[] { 12, 13, 14 };

         foreach (int[] array in arr1)       // Process the top level.          {             Console.WriteLine("Starting new array");             foreach (int item in array)      // Process the second level.             {                total += item;                Console.WriteLine("  Item: {0}, Current Total: {1}", item, total);             }          }       }    }`

该代码产生以下输出:


Starting new array   Item: 10, Current Total: 10   Item: 11, Current Total: 21 Starting new array   Item: 12, Current Total: 33   Item: 13, Current Total: 46   Item: 14, Current Total: 60


阵列协方差

在某些情况下,即使对象不是数组的基类型,也可以将对象分配给数组元素。数组的这个属性叫做数组协方差。如果满足以下条件,则可以使用数组协方差:

  • 该数组是引用类型数组。
  • 您正在分配的对象的类型和数组的基类型之间存在隐式或显式转换。

因为在派生类和它的基类之间总是有一个隐式转换,所以你总是可以把一个派生类的对象赋给一个为基类声明的数组。

例如,下面的代码声明了两个类,AB,其中类B派生自类A。最后一行通过将类型为B的对象分配给类型为A的数组元素来显示协方差。图 12-15 显示了代码的内存布局。

`   class A { ... }                                        // Base class    class B : A { ... }                                    // Derived class

   class Program {       static void Main() {          // Two arrays of type A[]          A[] AArray1 = new A[3];          A[] AArray2 = new A[3];

         // Normal--assigning objects of type A to an array of type A          AArray1[0] = new A(); AArray1[1] = new A(); AArray1[2] = new A();

         // Covariant--assigning objects of type B to an array of type A          AArray2[0] = new B(); AArray2[1] = new B(); AArray2[2] = new B();       }    }` Image

***图 12-15。*显示协方差的数组

Image 注意值类型数组没有协方差。

有用继承的数组成员

我之前提到过 C# 数组是从类System.Array派生的。它们从基类继承了许多有用的属性和方法。表 12-1 列出了一些最有用的方法。

Image

例如,下面的代码使用了其中的一些属性和方法:

`   public static void PrintArray(int[] a)    {       foreach (var x in a)          Console.Write("{0}  ", x);

      Console.WriteLine("");    }

   static void Main()    {       int[] arr = new int[] { 15, 20, 5, 25, 10 };       PrintArray(arr);

      Array.Sort(arr);       PrintArray(arr);

      Array.Reverse(arr);       PrintArray(arr);

      Console.WriteLine();       Console.WriteLine("Rank = {0}, Length = {1}",arr.Rank, arr.Length);       Console.WriteLine("GetLength(0)     = {0}",arr.GetLength(0));       Console.WriteLine("GetType()        = {0}",arr.GetType());    }`

该代码产生以下输出:


`15  20  5  25  10 5  10  15  20  25 25  20  15  10  5

Rank = 1, Length = 5 GetLength(0)     = 5 GetType()        = System.Int32[]`


克隆方法

方法执行数组的浅层复制。这意味着它只创建阵列本身的克隆。如果它是一个引用类型数组,它不会而不是复制元素引用的对象。这对于值类型数组和引用类型数组有不同的结果。

  • 克隆值类型数组会产生两个独立的数组。
  • 克隆引用类型数组会导致两个数组指向相同的对象。

Clone方法返回一个类型为object的引用,该引用必须转换为数组类型。

   int[] intArr1 = { 1, 2, 3 };                                         Array type                 Returns an object                                         <ins>    ↓   </ins>                        <ins>     ↓    </ins>    int[] intArr2 = ( int[] ) intArr1.Clone();

例如,下面的代码显示了一个克隆值类型数组的示例,生成两个独立的数组。图 12-16 说明了代码中显示的步骤。

`   static void Main()    {       int[] intArr1 = { 1, 2, 3 };                             // Step 1       int[] intArr2 = (int[]) intArr1.Clone();                 // Step 2

      intArr2[0] = 100; intArr2[1] = 200; intArr2[2] = 300;    // Step 3    }` Image

***图 12-16。*克隆一个值类型数组产生两个独立的数组。

克隆一个引用类型数组导致两个数组指向相同的对象。下面的代码显示了一个示例。图 12-17 说明了代码中显示的步骤。

`   class A    {       public int Value = 5;    }

   class Program    {       static void Main()       {          A[] AArray1 = new A[3] { new A(), new A(), new A() };     // Step 1          A[] AArray2 = (A[]) AArray1.Clone();                      // Step 2

         AArray2[0].Value = 100;          AArray2[1].Value = 200;          AArray2[2].Value = 300;                                   // Step 3       }    }` Image

***图 12-17。*克隆一个引用类型数组会产生两个引用相同对象的数组。

比较数组类型

表 12-2 总结了三种类型数组之间的一些重要的相似和不同之处。

Image

十三、委托

什么是委托?

您可以将委托想象成一个包含一个或多个方法的对象。当然,通常你不会想到“执行”一个对象,但是委托不同于一个典型的对象。您可以执行委托,当您这样做时,它会执行它“持有”的一个或多个方法

在这一章中,我将解释创建和使用委托的语法和语义。在后面的章节中,你将看到如何使用委托将可执行代码从一个方法传递到另一个方法——以及为什么这是一件有用的事情。

我们将从下一页的示例代码开始。如果在这一点上一切都不完全清楚,不要担心,因为我将在本章的其余部分解释委托的细节。

  • 代码从名为MyDel的委托类型的声明开始。(是的,一个委托——不是一个委托型对象。我们很快就会谈到这一点。)
  • Program声明了三个方法:PrintLowPrintHighMain。我们很快将创建的委托对象将持有PrintLowPrintHigh方法——但是使用哪一个要到运行时才能确定。
  • Main声明了一个名为del的局部变量,它将保存一个对MyDel类型的委托对象的引用。这并没有创建对象——它只是创建了一个变量,该变量将保存对 delegate 对象的引用,该对象将在下面几行创建并赋给它。
  • 创建一个 .NET 类Random,这是一个随机数生成器类。然后程序调用对象的Next方法,用99作为它的输入参数。这将返回一个 0 到 99 之间的随机整数,并将该值存储在本地变量randomValue中;。
  • 下一行检查返回和存储的随机值是否小于 50。(注意,我们在这里使用三元条件操作符来返回一个或另一个委托对象。)
    • 如果值小于 50,它创建一个MyDel委托对象并初始化它以保存对PrintLow方法的引用。
    • 否则,它会创建一个包含对PrintHigh方法的引用的MyDel委托对象。
  • 最后,Main 执行del委托对象,委托对象执行它持有的方法(PrintLowPrintHigh)。

Image 注意如果你来自 C++背景,理解委托的最快方法就是把它们想象成类型安全的、面向对象的 C++函数指针。

`   delegate void MyDel(int value);   // Declare delegate TYPE.

   class Program    {       void PrintLow( int value )       {          Console.WriteLine( "{0} - Low Value", value );       }

      void PrintHigh( int value )       {          Console.WriteLine( "{0} - High Value", value );       }

      static void Main( )       {          Program program = new Program();

         MyDel   del;            // Declare delegate variable.

         // Create random-integer-generator object and get a random          // number between 0 and 99.          Random  rand    = new Random();          int randomValue = rand.Next( 99 );

         // Create a delegate object that contains either PrintLow or          // PrintHigh, and assign the object to the del variable.          del = randomValue < 50                   ? new MyDel( program.PrintLow  )                   : new MyDel( program.PrintHigh );

         del( randomValue );    // Execute the delegate.       }    }`

因为我们使用的是随机数生成器,所以程序在不同的运行中会产生不同的值。该程序的一次运行产生了以下输出:


28 - Low Value


委托概述

现在让我们进入细节。委托是用户定义的类型,就像类是用户定义的类型一样。但是,类表示数据和方法的集合,而委托则包含一个或多个方法以及一组预定义的操作。

您可以通过执行以下步骤来使用委托。我将在下面的小节中详细介绍这些步骤。

  1. 声明一个委托类型。委托声明看起来像方法声明,只是它没有实现块。
  2. 声明委托类型的委托变量。
  3. 创建一个委托类型的对象,并将其赋给委托变量。新的委托对象包含对一个方法的引用,该方法必须具有与第一步中定义的委托类型相同的签名和返回类型。
  4. 您可以选择将其他方法添加到委托对象中。这些方法必须具有与第一步中定义的委托类型相同的签名和返回类型。
  5. 在整个代码中,您可以调用该委托,就像它是一个方法一样。当您调用委托时,它包含的每个方法都会被执行。

在查看前面的步骤时,您可能已经注意到它们类似于创建和使用类的步骤。图 13-1 比较了创建和使用类和委托的过程。

Image

***图 13-1。*委托是用户定义的引用类型,就像类一样。

你可以把委托想象成一个对象,它包含一个有序的方法列表,这些方法具有相同的签名和返回类型,如图 13-2 所示。

  • 方法列表被称为调用列表
  • 委托持有的方法可以来自任何类或结构,只要它们符合以下两个中的*:*
    • 委托的返回类型
    • 委托签名(包括refout修饰符)
  • 调用列表中的方法可以是实例方法,也可以是静态方法。
  • 当委托被调用时,它的调用列表中的每个方法都被执行。

Image

图 13-2 。作为方法列表的委托

声明委托类型

正如我在上一节中所说的,委托是类型,就像类是类型一样。与类一样,在使用委托类型创建变量和该类型的对象之前,必须声明委托类型。下面的代码示例声明了一个委托类型:

        Keyword      Delegate type name              ↓                      ↓    delegate void <ins>MyDel( int x )</ins>;              ↑        ↑                      Return type     Signature

委托类型的声明看起来很像方法的声明,因为它既有一个返回类型又有一个签名。返回类型和签名指定委托将接受的方法的形式。

前面的声明指定了类型为MyDel的委托对象将只接受具有单个int参数并且没有返回值的方法。图 13-3 显示了左边的委托类型和右边的委托对象。

Image

图 13-3 。委托类型和对象

委托类型声明在两个方面不同于方法声明。委托类型声明

  • 以关键字delegate开头
  • 没有方法体

Image 注意即使委托类型声明看起来像方法声明,它也不需要在类内声明,因为它是类型声明。

创建代理对象

委托是一种引用类型,因此既有引用又有对象。声明委托类型后,可以声明变量并创建该类型的对象。下面的代码显示了委托类型变量的声明:

   Delegate type     Variable                ↓              ↓       MyDel  delVar;

有两种方法可以创建委托对象。第一种是使用带有new操作符的对象创建表达式,如下面的代码所示。new运算符的操作数由以下内容组成:

  • 委托类型名称。
  • 一组括号,包含用作调用列表中第一个成员的方法的名称。方法可以是实例方法,也可以是静态方法。

                                            Instance method                                           <ins>             ↓             </ins> delVar = new MyDel( myInstObj.MyM1 );       // Create delegate and save ref. dVar   = new MyDel( <ins>SClass.OtherM2</ins> );       // Create delegate and save ref.                                                        ↑                       Static method

您也可以使用快捷语法,它只包含方法说明符,如下面的代码所示。这段代码和前面的代码在语义上是等价的。使用快捷语法是可行的,因为在方法名和兼容的委托类型之间存在隐式转换。

   delVar = myInstObj.MyM1;          // Create delegate and save reference.    dVar   = SClass.OtherM2;          // Create delegate and save reference.

例如,下面的代码创建了两个委托对象:一个使用实例方法,另一个使用静态方法。图 13-4 显示了代理的实例。这段代码假设有一个名为myInstObj的对象,它是一个类的实例,该类定义了一个名为MyM1的方法,该方法不返回值,并以一个int作为参数。它还假设有一个名为SClass的类,该类有一个静态方法OtherM2,其返回类型和签名与委托MyDel的返回类型和签名相匹配。

   delegate void MyDel(int x);               // Declare delegate type.    MyDel delVar, dVar;                       // Create two delegate variables.                                                    Instance method                        <ins>     ↓     </ins>    delVar = new MyDel( myInstObj.MyM1 );     // Create delegate and save ref.    dVar   = new MyDel( <ins>SClass.OtherM2</ins> );     // Create delegate and save ref.                                                               ↑                                                       Static method Image

图 13-4 。实例化委托

除了为委托分配内存之外,创建委托对象还会将第一个方法放在委托的调用列表中。

还可以使用初始化器语法,在同一语句中创建变量并实例化对象。例如,以下语句也会产生与图 13-4 中所示相同的配置:

   MyDel delVar = new MyDel( myInstObj.MyM1 );    MyDel dVar   = new MyDel( SClass.OtherM2 );

以下语句使用快捷语法,但同样产生如图 13-4 所示的结果:

   MyDel delVar = myInstObj.MyM1;    MyDel dVar   = SClass.OtherM2;

分配委托

因为委托是引用类型,所以可以通过给委托变量赋值来更改委托变量中包含的引用。旧的委托对象将被垃圾收集器(GC)处理掉。

例如,下面的代码设置然后改变delVar的值。图 13-5 说明了代码。

`   MyDel delVar;    delVar = myInstObj.MyM1;   // Create and assign the delegate object.

      ...    delVar = SClass.OtherM2;   // Create and assign the new delegate object.` Image

图 13-5 。给委托变量赋值

组合委托

到目前为止,您看到的所有委托在其调用列表中都只有一个方法。可以使用加法运算符“组合”委托。该操作的结果是创建一个新的委托,其调用列表是两个操作数委托的调用列表副本的串联。

例如,下面的代码创建三个委托。第三个委托是由前两个委托组合而成的。

`   MyDel delA = myInstObj.MyM1;    MyDel delB = SClass.OtherM2;

   MyDel delC = delA + delB;                  // Has combined invocation list`

尽管术语组合委托可能给人一种操作数委托被修改的印象,但它们根本没有改变。事实上,委托是不可改变的。委托对象创建后,不能更改。

图 13-6 展示了前面代码的结果。注意,操作数委托保持不变。

Image

图 13-6 。组合委托

向代理添加方法

尽管您在上一节中看到了委托实际上是不可变的,但是 C# 提供了语法,使您看起来可以使用+=操作符向委托添加方法。

例如,下面的代码将两个方法“添加”到委托的调用列表中。这些方法被添加到调用列表的底部。图 13-7 显示了结果。

   MyDel delVar  = inst.MyM1;     // Create and initialize.    delVar       += SCl.m3;        // Add a method.    delVar       += X.Act;         // Add a method. Image

图 13-7 。向委托“添加”方法的结果。实际上,因为委托是不可变的,所以调用列表中有三个方法的结果委托是一个由变量指向的全新委托。

当然,实际发生的是,当使用+=操作符时,一个新的委托被创建,调用列表是左边的委托和右边列出的方法的组合。这个新的委托然后被分配给delVar变量。

可以多次向委托添加方法。每次添加它时,它都会在调用列表中创建一个新元素。

从委托中删除方法

您也可以使用-=操作符从委托中删除一个方法。下面一行代码显示了操作符的用法。图 13-8 显示了该代码应用于图 13-7 中所示委托时的结果。

   delVar -= SCl.m3;             // Remove the method from the delegate. Image

图 13-8 。从委托中移除方法的结果

与向委托添加方法一样,产生的委托实际上是一个新委托。新委托是旧委托的副本,但是它的调用列表不再包含对被移除的方法的引用。

以下是删除方法时要记住的一些事情:

  • 如果一个方法在调用列表中有多个条目,那么-=操作符将从列表的底部开始搜索,并删除找到的第一个匹配方法的实例。
  • 尝试删除不在调用列表中的方法没有任何效果。
  • 试图调用空委托会引发异常。您可以通过将委托与null进行比较来检查委托的调用列表是否为空。如果调用列表为空,则委托为null

调用委托

你通过调用委托来调用它,就好像它只是一个方法一样。用于调用委托的参数用于调用调用列表上的每个方法(除非其中一个参数是输出参数,我将很快介绍这一点)。

例如,如下面的代码所示,委托delVar接受一个整数输入值。用参数调用委托会导致它用相同的参数值(本例中为 55)调用其调用列表中的每个成员。图 13-9 说明了调用。

   MyDel delVar  = inst.MyM1;    delVar       += SCl.m3;    delVar       += X.Act;       ...    delVar( 55 );                              // Invoke the delegate.       ... Image

***图 13-9。*当委托被调用时,它执行它的调用列表中的每个方法,使用与调用它时相同的参数。

如果一个方法不止一次出现在调用列表中,那么当委托被调用时,每次在列表中遇到该方法都会被调用。

委托示例

下面的代码定义并使用了一个没有参数和返回值的委托。请注意以下关于代码的内容:

  • Test定义了两个打印功能。
  • 方法Main创建委托的一个实例,然后再添加三个方法。
  • 然后程序调用委托,委托调用它的方法。然而,在调用委托之前,它检查以确保它不是null

`   // Define a delegate type with no return value and no parameters.    delegate void PrintFunction();

   class Test    {        public void Print1()        { Console.WriteLine("Print1 -- instance"); }

       public static void Print2()        { Console.WriteLine("Print2 -- static"); }    }

   class Program    {        static void Main()        {            Test t = new Test();    // Create a test class instance.            PrintFunction pf;       // Create a null delegate.

           pf = t.Print1;          // Instantiate and initialize the delegate.

           // Add three more methods to the delegate.            pf += Test.Print2;            pf += t.Print1;            pf += Test.Print2;            // The delegate now contains four methods.

           if( null != pf )           // Make sure the delegate isn't null.               pf();                   // Invoke the delegate.            else               Console.WriteLine("Delegate is empty");        }    }`

该代码产生以下输出:


Print1 -- instance Print2 -- static Print1 -- instance Print2 -- static


调用带有返回值的委托

如果委托在其调用列表中有一个返回值和多个方法,则会发生以下情况:

  • 调用列表中最后一个方法返回的值是委托调用返回的值。
  • 调用列表中所有其他方法的返回值都被忽略。

例如,下面的代码声明了一个返回int值的委托。创建委托的一个对象并添加两个额外的方法。然后,它调用WriteLine语句中的委托,并打印其返回值。图 13-10 显示了代码的图形表示。

`delegate int MyDel( );                 // Declare delegate with return value.    class MyClass {       int IntValue = 5;       public int Add2() { IntValue += 2; return IntValue;}       public int Add3() { IntValue += 3; return IntValue;}    }

   class Program {       static void Main( ) {          MyClass mc = new MyClass();          MyDel mDel = mc.Add2;          // Create and initialize the delegate.          mDel += mc.Add3;               // Add a method.          mDel += mc.Add2;               // Add a method.          Console.WriteLine("Value: {0}", mDel() );       }                                                                             ↑    }                       Invoke the delegate and use the return value.`

该代码产生以下输出:


Value: 12


Image

图 13-10 。最后执行的方法的返回值是委托返回的值。

用引用参数调用委托

如果委托有一个引用参数,该参数的值可以在从调用列表中的一个或多个方法返回时更改。

  • 当调用调用列表中的下一个方法时,参数的新值——而不是初始值—是传递给下一个方法的值。

例如,下面的代码调用带有引用参数的委托。图 13-11 说明了代码。

`   delegate void MyDel( ref int X );

   class MyClass    {       public void Add2(ref int x) { x += 2; }       public void Add3(ref int x) { x += 3; }       static void Main()       {          MyClass mc = new MyClass();

         MyDel mDel = mc.Add2;          mDel += mc.Add3;          mDel += mc.Add2;

         int x = 5;          mDel(ref x);

         Console.WriteLine("Value: {0}", x);       }    }`

该代码产生以下输出:


Value: 12


Image

图 13-11 。引用参数的值可以在调用之间改变。

匿名方法

到目前为止,您已经看到可以使用静态方法或实例方法来实例化委托。无论哪种情况,方法本身都可以从代码的其他部分显式调用,当然,必须是某个类或结构的成员。

但是,如果该方法只使用一次——来实例化委托,那该怎么办呢?在这种情况下,除了创建委托的语法要求之外,并不真正需要单独的命名方法。匿名方法允许您省去单独的命名方法。

  • 匿名方法是在实例化委托时内联声明的方法。

例如,图 13-12 显示了同一个类的两个版本。左边的版本声明并使用了一个名为Add20的方法。右边的版本使用匿名方法。两个版本的无阴影代码是相同的。

Image

图 13-12 。比较命名方法和匿名方法

图 13-12 中的两组代码产生以下输出:


25 26


使用匿名方法

您可以在以下位置使用匿名方法:

  • 声明委托变量时作为初始值设定项表达式。
  • 组合委托时位于赋值语句的右侧。
  • 在赋值语句的右侧,向事件添加委托。第十四章报道事件。
匿名方法的语法

匿名方法表达式的语法包括以下组件:

  • 键入关键字delegate
  • 参数列表,如果语句块不使用任何参数,可以省略
  • 语句块,它包含匿名方法的代码

                                Parameter         Keyword               list                         Statement block              ↓                     ↓                     <ins>                     ↓                     </ins>    delegate ( Parameters )  { ImplementationCode }

返回类型

匿名方法不显式声明返回类型。但是,实现代码本身的行为必须通过返回该类型的值来匹配委托的返回类型。如果委托的返回类型为void,那么匿名方法代码不能返回值。

例如,在下面的代码中,委托的返回类型是int。因此,匿名方法的实现代码必须在通过代码的所有路径上返回一个int

`              Return type of delegate type                            ↓    delegate int OtherDel(int InParam);

   static void Main()    {        OtherDel del = delegate(int x)                    {                        return x + 20 ;                   // Returns an int                    };           ...    }`

参数

除了数组参数之外,匿名方法的参数列表必须与委托的参数列表在以下三个方面相匹配:

  • 参数数量
  • 参数的类型和位置
  • 修饰语

您可以通过将括号留空或完全省略来简化匿名方法的参数列表,但前提是以下两个都为真:

  • 委托的参数列表不包含任何out参数。
  • 匿名方法不使用任何参数。

例如,下面的代码声明了一个没有任何out参数的委托和一个不使用任何参数的匿名方法。因为这两个条件都满足,所以可以从匿名方法中省略参数列表。

   delegate void SomeDel ( int X );                 // Declare the delegate type.       SomeDel SDel = delegate                          // Parameter list omitted                   {                      PrintMessage();                      Cleanup();                   };

params 参数

如果委托声明的参数列表包含一个params参数,那么params关键字将从匿名方法的参数列表中省略。例如,在下面的代码中:

  • 委托类型声明将最后一个参数指定为params类型参数。
  • 然而,匿名方法的参数列表必须省略params关键字。

              params keyword used in delegate type declaration                                                                     ↓    delegate void SomeDel( int X, params int[] Y);                                         params keyword omitted in matching anonymous method                                                                       ↓    SomeDel mDel = delegate (int X, int[] Y)             {                ...             };

变量和参数的范围

匿名方法中声明的参数和局部变量的作用域被限制在实现代码的主体内,如图图 13-13 所示。

例如,下面的匿名方法定义了参数y和局部变量z。匿名方法的主体关闭后,yz不再在作用域内。代码的最后一行会产生一个编译错误。

Image

图 13-13 。变量和参数的范围

外部变量

与委托的命名方法不同,匿名方法可以访问局部变量及其周围的环境。

  • 来自周围作用域的变量称为外部变量
  • 匿名方法的实现代码中使用的外部变量被称为由该方法捕获的。

例如,图 13-14 中的代码显示了在匿名方法之外定义的变量x。然而,方法中的代码可以访问x并打印其值。

Image

图 13-14 。使用外部变量

延长捕获变量的生命周期

只要捕获的外部变量的捕获方法是委托的一部分,该变量就会保持活动状态,即使该变量通常会超出范围。

例如,图 13-15 中的代码说明了一个被捕获变量生命周期的延长。

  • 局部变量x在块内声明并初始化。
  • 委托mDel然后被实例化,使用一个匿名方法捕获外部变量x
  • 当块关闭时,x超出范围。
  • 如果块结束后的WriteLine语句被取消注释,将会导致编译错误,因为它引用了x,而后者现在超出了范围。
  • 然而,委托mDel中的匿名方法在其环境中维护x,并在mDel被调用时打印其值。

Image

图 13-15 。匿名方法中捕获的变量

图中的代码产生以下输出:


Value of x: 5


λ表达式

C# 2.0 引入了匿名方法,我们刚才已经讨论过了。然而,匿名方法的语法有些冗长,并且需要编译器本身已经知道的信息。C# 3.0 没有要求您包含这些冗余信息,而是引入了 lambda 表达式,减少了匿名方法的语法。你可能想要使用 lambda 表达式而不是匿名方法。事实上,如果 lambda 表达式被首先引入,就不会有匿名方法。

在匿名方法语法中,delegate关键字是多余的,因为编译器已经可以看出您正在将方法分配给委托。通过执行以下操作,可以轻松地将匿名方法转换为 lambda 表达式:

  • 删除delegate关键字。
  • 将 lambda 运算符=>放在参数列表和匿名方法体之间。lambda 运算符读作“goes to”

下面的代码展示了这种转换。第一行显示了一个匿名方法被分配给变量del。第二行显示了相同的匿名方法,在被转换成 lambda 表达式后,被赋给变量le1

   MyDel del = delegate(int x)    { return x + 1; } ;     // Anonymous method    MyDel le1 =         (int x) => { return x + 1; } ;     // Lambda expression

Image 术语λ表达式来源于数学家阿隆佐·邱奇等人在 20 世纪二三十年代发展起来的λ演算。lambda 演算是一个表示函数的系统,使用希腊字母 lambda()来表示一个无名函数。最近,函数式编程语言(如 Lisp 及其方言)使用该术语来表示表达式,这些表达式可用于直接描述函数的定义,而不是为函数命名。

这个简单的转换不太冗长,看起来更干净,但是它只节省了六个字符。然而,编译器可以推断出更多的东西,从而允许您进一步简化 lambda 表达式,如下面的代码所示。

  • 从委托的声明中,编译器也知道委托参数的类型,因此 lambda 表达式允许您省略参数类型,如对le2的赋值所示。
    • 与其类型一起列出的参数称为显式类型化的
    • 那些没有列出类型的被称为隐式类型
  • 如果只有一个隐式类型的参数,可以去掉括号,如对le3的赋值所示。
  • 最后,lambda 表达式允许表达式的主体是语句块或者表达式。如果语句块包含一个 return 语句,可以用跟在关键字return后面的表达式来替换语句块,如对le4的赋值所示。

   MyDel del = delegate(int x)    { return x + 1; } ;     // Anonymous method    MyDel le1 =         (int x) => { return x + 1; } ;     // Lambda expression    MyDel le2 =             (x) => { return x + 1; } ;     // Lambda expression    MyDel le3 =              x  => { return x + 1; } ;     // Lambda expression    MyDel le4 =              x  =>          x + 1    ;     // Lambda expression

lambda 表达式的最终形式大约只有原始匿名方法的四分之一,而且更清晰、更容易理解。

下面的代码展示了完整的转换。Main的第一行显示了一个匿名方法被分配给变量del。第二行显示了同样的匿名方法,在被转换成 lambda 表达式后,被赋给变量le1

`   delegate double MyDel(int par);

   class Program    {          static void Main()       {          MyDel del = delegate(int x)    { return x + 1; } ;  // Anonymous method

         MyDel le1 =         (int x) => { return x + 1; } ;  // Lambda expression          MyDel le2 =             (x) => { return x + 1; } ;          MyDel le3 =              x  => { return x + 1; } ;          MyDel le4 =              x  =>          x + 1    ;

         Console.WriteLine("{0}", del (12));          Console.WriteLine("{0}", le1 (12));  Console.WriteLine("{0}", le2 (12));          Console.WriteLine("{0}", le3 (12));  Console.WriteLine("{0}", le4 (12));       }    }`

该代码产生以下输出:


13 13 13 13 13


关于 lambda 表达式参数表的一些要点如下:

  • lambda 表达式的参数列表中的参数在数量、类型和位置上必须与委托的参数匹配。
  • 表达式的参数列表中的参数不必包含类型(即,它们是隐式类型化的),除非委托有refout参数——在这种情况下,类型是必需的(即,它们是显式类型化的)。
  • 如果只有一个参数,并且是隐式类型的,则可以省略括号。否则,它们是必需的。
  • 如果没有参数,则必须使用一组空括号。

图 13-16 显示了 lambda 表达式的语法。

Image

***图 13-16。*lambda 表达式的语法由 lambda 运算符组成,左边是参数部分,右边是 lambda 主体。

十四、事件

发布者和订阅者

许多程序中的一个常见需求是,当一个特定的程序事件发生时,程序的其他部分需要被通知该事件已经发生。

满足这个需求的一种模式叫做发布者/订阅者模式。在这种模式中,一个名为发布者的类定义了一组程序的其他部分可能感兴趣的事件。当这些事件发生时,其他类可以“注册”得到发布者的通知。这些订阅者类通过向发布者提供一个方法来“注册”通知。当事件发生时,发布者“引发事件”,订阅者提交的所有方法都被执行。

订阅者提供的方法被称为回调方法,因为发布者通过执行它们的方法“回调订阅者”。它们也被称为事件处理程序,因为它们是被调用来处理事件的代码。图 14-1 展示了这个过程,显示了一个事件的发布者和该事件的三个订阅者。

Image

图 14-1 。发布者和订阅者

以下是一些与事件相关的重要术语:

  • Publisher: 发布事件的类或结构,以便在事件发生时通知其他类。
  • Subscriber :一个类或结构,当事件发生时,它注册以得到通知。
  • 事件处理程序:由订阅者向发布者注册的方法,当发布者引发事件时执行。事件处理程序方法可以在与事件相同的类或结构中声明,也可以在不同的类或结构中声明。
  • 引发事件:是调用触发事件的术语。当引发一个事件时,所有用该事件注册的方法都会被调用。

前一章讨论了代表。事件的许多方面与代表的相似。事实上,事件就像一个更简单的委托,专门用于特定的用途。代表和事件的行为有很好的相似性。一个事件包含一个私有委托,如图图 14-2 所示。

关于事件的私人代表,需要了解的重要事项如下:

  • 一个事件给出了对其私有控制的委托的结构化访问。也就是说,您不能直接访问该委托。
  • 可用的操作比委托少。对于事件,您只能添加和移除事件处理程序,以及调用事件。
  • 当事件被引发时,它调用委托,委托依次调用其调用列表中的方法。

注意在图 14-2 中,只有+=-=操作符突出到事件框的左侧。这是因为它们是事件上唯一允许的操作(除了调用事件本身)。

Image

***图 14-2。*一个事件有一个封装的委托。

图 14-3 展示了一个名为Incrementer的程序,它执行某种计数。

  • Incrementer定义了一个名为CountedADozen的事件,每当它计算另外十几个项目时就会引发这个事件。
  • 订户类DozensSomeOtherClass都有一个用CountedADozen事件注册的事件处理程序。
  • 每次引发事件时,都会调用处理程序。

Image

***图 14-3。*一个事件类的结构和术语

源代码组件概述

使用事件需要五段代码。这些在图 14-4 中进行了说明。我将在接下来的章节中逐一介绍它们。这些代码如下所示:

  • 委托类型声明:事件和事件处理程序必须有共同的签名和返回类型,由委托类型描述。
  • 事件处理程序声明:这些是订阅者类中的声明,当事件被引发时,这些方法将被执行。这些不必是显式命名的方法;它们也可以是匿名方法或 lambda 表达式,如第十三章所述。
  • 事件声明:发布者类必须声明一个订阅者可以注册的事件成员。当一个类声明了一个public事件时,就说是发布了事件
  • 事件注册:订阅者必须注册一个事件,以便在事件发生时得到通知。这是将事件处理程序连接到事件的代码。
  • 引发事件的代码:这是发布者中“激发”事件的代码,导致它调用所有向它注册的事件处理程序。

Image

***图 14-4。*使用事件的五个源代码组件

宣告一个事件

发布者必须提供事件对象。创建事件很简单——它只需要一个委托类型和一个名称。事件声明的语法如下面的代码所示,它声明了一个名为CountedADozen的事件。请注意关于事件CountedADozen的以下内容:

  • 事件是在类中声明的。
  • 它需要委托类型的名称。任何附加到事件的事件处理程序(即注册到事件)都必须在签名和返回类型上与委托类型相匹配。
  • 它被声明为public,以便其他类和结构可以向它注册事件处理程序。
  • 您不能对事件使用对象创建表达式(一个new表达式)。

   class Incrementer    {                      Keyword                            Name of event                                           ↓                                           ↓             public event EventHandler CountedADozen;                                                    ↑                                             Delegate type

通过使用逗号分隔的列表,可以在声明语句中声明多个事件。例如,以下语句声明了三个事件:

   public event EventHandler <ins>MyEvent1, MyEvent2, OtherEvent</ins>;                                                                                                 ↑                                                                                      Three events

您还可以通过包含static关键字使事件成为静态的,如下面的声明所示:

public static event EventHandler CountedADozen;                          ↑                                                 Keyword

一个事件是一个成员

一个常见的错误是认为事件是一种类型,但事实并非如此。像方法或属性一样,事件是类或结构的成员,这有几个重要的分支:

  • 因为事件是成员
    • 不能在可执行代码块中声明事件。
    • 它必须在类或结构中用其他成员声明。
  • 事件成员与其他成员一起被隐式地自动初始化为null

要声明一个事件,您必须提供一个委托类型的名称。您可以声明一个或者使用一个已经存在的。如果声明委托类型,它必须指定将由事件注册的方法的签名和返回类型。

BCL 声明了一个名为EventHandler的委托,专门用于系统事件。我将在本章的后面描述EventHandler委托。

订阅一个事件

订阅者向事件添加事件处理程序。对于要添加到事件中的事件处理程序,该处理程序必须与事件的委托具有相同的返回类型和签名。

  • 使用+=运算符向事件添加事件处理程序,如下面的代码所示。事件处理程序放在操作符的右边。
  • 事件处理程序规范可以是以下任何一种:
    • 实例方法的名称
    • 静态方法的名称
    • 匿名方法
    • λ表达式

例如,下面的代码向事件CountedADozen添加了三个方法。第一个是实例方法。第二种是静态方法。第三种是实例方法,使用委托形式。

       Class instance                                           Instance method                ↓                                                                ↓                      incrementer.CountedADozen += IncrementDozensCount;        // Method reference form    incrementer.CountedADozen += <ins>ClassB.CounterHandlerB</ins>;      // Method reference form                                          ↑                                           ↑                                 Event member                           Static method    mc.CountedADozen += new EventHandler(cc.CounterHandlerC);  // Delegate form

就像委托一样,您可以使用匿名方法和 lambda 表达式来添加事件处理程序。例如,下面的代码首先使用 lambda 表达式,然后使用匿名方法。

`   // Lambda expression    incrementer.CountedADozen += () => DozensCount++;

   // Anonymous method    incrementer.CountedADozen += delegate { DozensCount++; };`

引发一个事件

事件成员本身只保存需要调用的事件处理程序。除非引发事件,否则它们不会发生任何事情。您需要确保在适当的时候有代码可以做到这一点。

例如,以下代码引发事件CountedADozen。请注意以下关于代码的内容:

  • 在引发事件之前,代码将它与null进行比较,看它是否包含任何事件处理程序。如果事件为null,则为空,无法执行。
  • 引发事件的语法与调用方法的语法相同:
    • 使用事件的名称,后跟用括号括起来的参数列表。
    • 参数列表必须与事件的委托类型匹配。

   if (CountedADozen != null)              // Make sure there are methods to execute.       CountedADozen (<ins>source, args</ins>);        // Raise the event.                    ↑                           ↑       Event name                   Parameter list

将事件声明和引发事件的代码放在一起,为发布者提供了以下类声明。代码包含两个成员:事件和一个名为DoCount的方法,该方法在适当的时候引发事件。

`   class Incrementer    {       public event EventHandler CountedADozen;   // Declare the event.

      void DoCount(object source, EventArgs args)       {          for( int i=1; i < 100; i++ )             if( i % 12 == 0 )                if (CountedADozen != null)       // Make sure there are methods to execute.                   CountedADozen(source, args);       }                                           ↑                                              Raise the event.    }`

图 14-5 中的代码显示了整个程序,发布者类Incrementer和订阅者类Dozens。关于代码需要注意的事项如下:

  • 在其构造函数中,类Dozens订阅事件,提供方法IncrementDozensCount作为其事件处理程序。
  • 在类Incrementer的方法DoCount中,每当该方法再增加 12 次时,就会引发事件CountedADozen

Image

**图 14-5。**一个完整的程序,有一个发布者和一个订阅者,展示了使用一个事件所需的五段代码

图 14-5 中的代码产生以下输出:


Number of dozens = 8


标准事件用法

GUI 编程是事件驱动的,这意味着当程序运行时,它可以随时被诸如按钮点击、按键或系统定时器之类的事件中断。当这种情况发生时,程序需要处理事件,然后继续它的进程。

显然,这种程序事件的异步处理是使用 C# 事件的最佳场合。Windows GUI 编程广泛使用事件,因此有一个标准 .NET 框架模式来使用它们。事件使用的标准模式的基础是在System名称空间中声明的EventHandler委托类型。下面一行代码显示了EventHandler委托类型的声明。关于声明需要注意的事项如下:

  • 第一个参数用于保存对引发事件的对象的引用。它的类型是object,因此可以匹配任何类型的任何实例。
  • 第二个参数用于保存适用于应用的任何类型的状态信息。
  • 返回类型为void

   public delegate void EventHandler(object sender, EventArgs e);

EventHandler委托类型中的第二个参数是类EventArgs的一个对象,它在System命名空间中声明。您可能会想,既然第二个参数是用来传递数据的,那么一个EventArgs类对象应该能够存储某种类型的数据。你就错了。

  • EventArgs类被设计成不携带数据。它用于不需要传递数据的事件处理程序,通常被它们忽略。
  • 如果你想传递数据,你必须声明一个从EventArgs派生出的类*,用适当的字段来保存你想传递的数据。*

尽管EventArgs类实际上并不传递数据,但它是使用EventHandler委托模式的重要组成部分。这些类型为objectEventArgs的参数是用作参数的任何实际类型的基类。这允许EventHandler委托提供一个签名,它是所有事件和事件处理程序的最小公分母,允许所有事件正好有两个参数,而不是每个事件有不同的签名。

如果我们修改Incrementer程序来使用EventHandler委托,我们就有了如图图 14-6 所示的程序。请注意以下关于代码的内容:

  • 委托Handler的声明已被删除,因为事件使用系统定义的EventHandler委托。
  • subscriber 类中事件处理程序声明的签名必须与事件委托的签名(和返回类型)相匹配,事件委托现在使用类型为objectEventArgs的参数。在事件处理程序IncrementDozensCount的情况下,该方法只是忽略了形式参数。
  • 引发事件的代码必须使用适当参数类型的对象来调用事件。

Image

***图 14-6。*increment er 程序修改为使用系统定义的 EventHandler 委托

通过扩展 EventArgs 传递数据

要在事件处理程序的第二个参数中传递数据并遵守标准约定,您需要声明一个从EventArgs派生的自定义类,它可以存储您需要传递的数据。类名应该以EventArgs结尾。例如,下面的代码声明了一个自定义类,它可以在名为Message的字段中存储一个字符串:

                                    Custom class name              Base class                                                   ↓                                ↓       public class IncrementerEventArgs : EventArgs    {       public int IterationCount { get; set; }  // Stores an integer    }

现在,您已经有了一个用于在事件处理程序的第二个参数中传递数据的自定义类,您需要一个使用新自定义类的委托类型。要实现这一点,请使用委托的通用版本EventHandler<>。第十七章详细介绍了 C# 泛型,所以现在你只能看着。若要使用泛型委托,请执行下列操作,如后续代码所示:

  • 将自定义类的名称放在尖括号之间。
  • 在应该使用自定义委托类型名称的地方使用整个字符串。例如,event声明应该是这样的:

                                       Generic delegate using custom class                                  <ins>                                ↓                               </ins>    public event EventHandler<IncrementerEventArgs> CountedADozen;                                                                                                                            ↑                                                                                                                                 Event name

在处理事件的其他四段代码中使用自定义类和自定义委托。例如,下面的代码更新了Incrementer代码,以使用名为IncrementerEventArgs的定制EventArgs类和通用EventHandler<IncrementerEventArgs>委托。

`public class IncrementerEventArgs : EventArgs   // Custom class derived from EventArgs    {       public int IterationCount { get; set; }      // Stores an integer    }

   class Incrementer      Generic delegate using custom class    {                              ↓                      public event EventHandler CountedADozen;

      public void DoCount()      Object of custom class       {                                ↓               IncrementerEventArgs args = new IncrementerEventArgs();          for ( int i=1; i < 100; i++ )             if ( i % 12 == 0 && CountedADozen != null )             {                args.IterationCount = i;                CountedADozen( this, args );             }                     ↑       }                                      Pass parameters when raising the event    }

   class Dozens    {       public int DozensCount { get; private set; }

      public Dozens( Incrementer incrementer )       {          DozensCount = 0;          incrementer.CountedADozen += IncrementDozensCount;       }

      void IncrementDozensCount( object source, IncrementerEventArgs e )       {          Console.WriteLine( "Incremented at iteration: {0} in {1}",                                         e.IterationCount, source.ToString() );          DozensCount++;       }    }

   class Program    {       static void Main()       {          Incrementer incrementer = new Incrementer();          Dozens dozensCounter    = new Dozens( incrementer );

         incrementer.DoCount();          Console.WriteLine( "Number of dozens = {0}",                                  dozensCounter.DozensCount );       }    }`

这个程序产生下面的输出,它显示了调用它时的迭代和源对象的完全限定类名。我将在第二十一章中讨论全限定类名。


Incremented at iteration: 12 in Counter.Incrementer Incremented at iteration: 24 in Counter.Incrementer Incremented at iteration: 36 in Counter.Incrementer Incremented at iteration: 48 in Counter.Incrementer Incremented at iteration: 60 in Counter.Incrementer Incremented at iteration: 72 in Counter.Incrementer Incremented at iteration: 84 in Counter.Incrementer Incremented at iteration: 96 in Counter.Incrementer Number of dozens = 8


删除事件处理程序

当您完成一个事件处理程序时,您可以将它从事件中移除。使用-=操作符从事件中移除事件处理程序,如下面的代码行所示:

   p.SimpleEvent -= s.MethodB;;         // Remove handler MethodB.

例如,以下代码向事件SimpleEvent添加两个处理程序,然后引发事件。每个处理程序都被调用并打印出一行文本。然后从事件中移除MethodB处理程序,当事件再次被引发时,只有MethodB处理程序打印出一行。

`   class Publisher    {       public event EventHandler SimpleEvent;

      public void RaiseTheEvent() { SimpleEvent( this, null ); }    }

   class Subscriber    {       public void MethodA( object o, EventArgs e ) { Console.WriteLine( "AAA" ); }       public void MethodB( object o, EventArgs e ) { Console.WriteLine( "BBB" ); }    }

   class Program    {       static void Main( )       {          Publisher  p = new Publisher();          Subscriber s = new Subscriber();

         p.SimpleEvent += s.MethodA;          p.SimpleEvent += s.MethodB;          p.RaiseTheEvent();

         Console.WriteLine( "\r\nRemove MethodB" );          p.SimpleEvent -= s.MethodB;          p.RaiseTheEvent();       }    }`

该代码产生以下输出:


`AAA BBB

Remove MethodB AAA`


如果一个处理程序在一个事件中注册了多次,那么当您发出删除该处理程序的命令时,只会从列表中删除该处理程序的最后一个实例。

事件访问器

本章的最后一个主题是事件访问器。我前面提到过,+=-=操作符是事件中唯一允许的操作符。这些操作符具有你在本章中已经看到的定义良好的行为。

但是,可以改变这些操作符的行为,让事件在使用它们时执行您喜欢的任何自定义代码。然而,这是一个高级的话题,所以我在这里只提一下,不涉及太多的细节。

若要更改这些运算符的操作,必须为事件定义事件访问器。

  • 有两个访问器:addremove
  • 带有访问器的事件声明看起来类似于属性声明。

下面的示例显示了带有访问器的事件声明的形式。这两个访问器都有一个名为value的隐式值参数,它接受对实例方法或静态方法的引用。

`   public event EventHandler CountedADozen    {       add       {          ...                            // Code to implement the =+ operator       }

      remove       {          ...                            // Code to implement the -= operator       }    }`

当声明事件访问器时,事件不包含嵌入的委托对象。您必须实现自己的存储机制来存储和移除用事件注册的方法。

事件访问器充当 void 方法,这意味着它们不能使用返回值的 return 语句。