C# 学习笔记(四)

结构

什么是结构

和类类似,也有数据成员和函数成员,但结构是值类型,而类是引用类型,且结构是隐式密封的,不能派生其他结构

结构是值类型

和所有的值类型一样,结构类型的变量不能为 null,且两个结构变量不能引用同一对象

对结构赋值

和其他值类型一样,是值复制,而非引用的复制

构造函数和析构函数

结构可以有实例构造函数和静态构造函数,但不允许有析构函数

实例构造函数

在书里会提到 struct 提供了隐式的无参构造函数,且不能删除和重定义,不过从 C# 10 开始,struct 就支持显式的自定义无参构造函数了,不过必须是 public,且不能是 partial。默认的无参构造函数会强制把所有变量初始化为默认值,也就是值类型一般是 0,引用类型是 null。在 C# 11 之前,结构类型的构造函数必须初始化该类型的所有实例字段

struct Simple
{
    public int X = 3;
    public int Y = 4;

    // public Simple()
    // {
    // }

    public Simple(int x, int y)
    {
        X = x;
        Y = y;
    }
}

class Program
{
    static void Main()
    {
        Simple s1 = new Simple(); // 调用隐式构造函数
        Simple s2 = new Simple(15, 10); // 调用构造函数
        Console.WriteLine($"s1: {s1.X}, {s1.Y}");
        Console.WriteLine($"s2: {s2.X}, {s2.Y}");
    }
}
// s1: 0, 0 被默认的无参构造函数覆盖了值,显式定义一个空的,就是 3, 4了
// s2: 15, 10

因为 struct 不是引用类型,所以,创建时也可以不使用 new 运算符,不过这样就有一些限制

  • 在显式设置了数据成员后,才能使用它们的值
  • 在对所有数据成员赋值之后,才能调用结构的函数成员

不使用 new 初始化的话,struct 也和其他值类型一样处于一个未定义状态,不会执行构造函数,因此也不能读取其成员,即便 C# 10 之后,在声明数据成员时进行了初始化也一样。另外值得注意的是,即便使用 new 关键字,struct 创建的也是值,不是引用,C# 的 new 和 C++ 的不同,并不决定存储位置和类型,只影响是否初始化,是否调用构造函数。

静态构造函数

基本同类的静态构造函数,且没有不能定义无参静态构造函数的限制

属性和字段初始化语句

同样的在 C# 10.0 之前,struct 也无法像类一样,在声明字段的同时进行初始化,现在新版的话,在有显式的构造函数时,可以在声明的同时进行初始化了。

结构是密封的

因为结构是隐式密封的,因此以下几个继承相关的修饰符自然也不能使用了

  • protected
  • protected internal
  • abstract
  • sealed
  • virtual

不过因为结构本身是派生自 System.ValueType,而 System.ValueType 又派生自 object,所以 newoverride 修饰符是可以使用的,可以用于覆盖或重写一些基类的成员。

结构作为返回值和参数

和其他值类型没啥区别

关于结构的更多内容

因为是值类型,且限制较多,所以开销也比类小,用结构替代类有时可以提高性能,但需要注意装箱和拆箱的高昂代价

  • 预定义简单类型(int、short、long,等等),尽管在 .NET 和 C# 中都视为原始类型,但它们实际上在 .NET 都实现为结构
  • 可以使用与声明分部类相同的方法声明分部结构
  • 结构和类一样,也可以是实现接口

枚举

枚举和类与结构一样,都是程序员自定义类型,和结构一样都是值类型,枚举只有一种类型的成员:命名的整数值常量

用法上和 C++ 的 enum 区别不大,每个枚举类型都有一个底层的整数类型,默认为 int

  • 每个枚举成员都被赋予一个底层类型的常量值
  • 在默认情况下,编译器对第一个成员赋值为 0,对每一个后续成员赋的值都比前一个多 1
enum TrafficLight
{
    Green,
    Yellow,
    Red
}

class Program
{
    static void Main()
    {
        TrafficLight t1 = TrafficLight.Green;
        TrafficLight t2 = TrafficLight.Yellow;
        TrafficLight t3 = TrafficLight.Red;

        Console.WriteLine($"{t1},\t{(int)t1}");
        Console.WriteLine($"{t2},\t{(int)t2}");
        Console.WriteLine($"{t3},\t{(int)t3}");
    }
}
// Green,  0
// Yellow, 1
// Red,    2

设置底层类型和显式值

和 C++ 一样,可以把冒号加类型名,放在枚举名之后,使用 int 以外的其他整数类型,同样的也可以显式的指定一个成员的值,值可以重复,但成员的名称不能重复

enum TrafficLight : ulong
{
    Green = 10,
    Yellow = 15,
    Red = 15
}

隐式成员编号

同 C++,没显式初始化的,就默认为上一个成员的值+1

位标记

程序员长期使用单个字的不同位作为表示一组开/关标志的紧凑方法,将其称为标志字。枚举提供了简单实现它的简便方法

一般步骤如下

  1. 确定需要多少个标志位,并选择一种足够多位的无符号类型来保存它
  2. 确定每个位置代表什么,并给它们一个名称。声明一个选中的整数类型枚举,每个成员由一个位位置表示
  3. 使用按位或运算符在持有该位标志的字中设置适当的位
  4. 使用按位与运算符或 HasFlag 方法检查是否设置了特定位标志
  • 成员有表示二进制选项的名称
    • 每个选项由字中的一个特定位置表示
    • 因为一个位标志表示一个或开或关的位,所以你不会想用0作为成员值。它已经有了一个含义:所有标志位都是关
  • 在十六进制表示法中,每个十六进制数字由 4 位来表示。由于位模式和十六进制之间的直接联系,所以经常使用十六进制
  • C# 7.0 开始,可以使用二进制标识法了
  • 使用 Flags 特性装饰枚举不是必要的,但可以带来一些便利
[Flags]
enum CardDeckSettings : uint
{
    SingleDeck = 0x01,
    LargePictures = 0x02,
    FancyNumbers = 0x04,
    Animation = 0x08
}

HasFlag 可以一次检测多个标志位

CardDeckSettings ops = CardDeckSettings.SingleDeck
                       | CardDeckSettings.FancyNumbers
                       | CardDeckSettings.Animation;
CardDeckSettings testFlags = CardDeckSettings.Animation | CardDeckSettings.FancyNumbers;
bool useFancyNumbers = ops.HasFlag(testFlags);

Flags 特性

Flags 特性不会改变计算结果,但会提供一些方便的特性,它会通知编译器、对象浏览器以及其他查看这段代码的工具,该枚举的成员不仅可以作为单独的值,还可以组合成位标志。如果没有 Flags 特性,输出如下

enum CardDeckSettings : uint
{
    SingleDeck = 0x01,
    LargePictures = 0x02,
    FancyNumbers = 0x04,
    Animation = 0x08
}

class Program
{
    static void Main()
    {
        CardDeckSettings ops = CardDeckSettings.FancyNumbers;
        Console.WriteLine(ops);
        ops = CardDeckSettings.FancyNumbers | CardDeckSettings.Animation;
        Console.WriteLine(ops);
    }
}
// FancyNumbers
// 12
// 如果有 Flags 特性,则为 FancyNumbers, Animation

关于枚举的更多内容

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

  • 不能对成员使用修饰符。它们都隐式的具有和枚举相同的可访问性
  • 由于成员是静态的,即使在没有该枚举类型的变量时也可以访问它们
  • 和所有静态类型一样,可以使用 using static 指令来避免每次使用都包含类名
  • 由于枚举是一个独特的类型,因此比较不同枚举类型的成员会导致编译错误

数组

数组

定义

基本同 C++ 数组

重要细节

同 C++ 数组,一旦创建,大小固定,索引号从 0 开始

数组的类型

一维数组和 C++ 的区别不大,C# 的多维数组有两种

  • 矩形数组
    • 和 C++ 的多维数组一样,每个子数组的长度相等
    • 形式上不同维度都在一个方括号里:int x = myArray[4,6,1];
  • 交错数组
    • 每个子数组都是独立数组
    • 可以有不同长度的子数组
    • 形式上和 C++ 的多维数组一样,一个维度一个方括号:jagArray1[2][7][4]

数组是对象

数组实例是从 System.Array 继承类型的对象。由于数组从 BCL 基类派生而来,它们也继承了 BCL 基类中很多有用的成员

  • Rank 返回数组维度数的属性
  • Length 返回数组长度的属性

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

  • 如果存储的元素都是值类型,数组被称为值类型数组
  • 如果存储的元素都是引用类型,数组被称为引用类型数组

一维数组和矩形数组

C# 的数组声明和 C++ 不同,只能声明秩也就是维度数量,而不能声明每个维度的长度,且方括号在基类型后,而不是变量名称后

int[,,] firstArray; // 三维整型数组
int[,] arr1; // 二维整型数组
long[,,] arr2; // 三维长整型数组

// long[3, 2, 6] SecondArray; // 编译错误

实例化一维数组或矩形数组

形式上类似 C++ 用 new 创建数组

int[] arr2 = new int[4];
MyClass[,,] mcArr = new MyClass[3, 6, 2];

访问数组元素

和 C++ 没什么区别,就是矩形数组都在一个方括号里

初始化数组

数组被创建后,每一个元素都自动被初始化为类型的默认值

显式初始化一维数组

类似 C++ 使用初始化列表的方式,可以不写维度长度,编译器会自动推导

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

显式初始化二维数组

同上

int[,] intarray2 = new int[,] { { 10, 1 }, { 2, 10 }, { 11, 9 } };

快捷语法

和 C++ 一样,可以直接用初始化列表赋值初始化

int[,] intarray2 = { { 10, 1 }, { 2, 10 }, { 11, 9 } };

隐式类型数组

同样的,也可以通过 var 来进行类型推导,后面的 new 只能省略类型,不能省略秩

var intarray2 = new[,] { { 10, 1 }, { 2, 10 }, { 11, 9 } }; // 这是最省略的写法

交错数组

交错数组是真正意义上的数组的数组,每个子数组都是独立存储的,而矩形数组是一片完整的内存

声明交错数组

声明方式和矩形数组一样,只不过使用多个方括号而非逗号,同样的,不能在声明中指定维度长度

快捷实例化

因为交错数组,每个子数组的长度可以不同,因此实例化时,只能指定顶层的维度长度

int[][] jagArr = new int[3][];

实例化交错数组

实例化完整的交错数组分两步,第一步实例化顶层数组,第二步分别实例化每一个子数组

int[][] Arr = new int[3][];
Arr[0] = new int[] { 10, 20, 30 };
Arr[1] = new int[] { 40, 50, 60, 70 };
Arr[2] = new int[] { 80, 90, 100, 110, 120 };

交错数组中的子数组

因为每个子数组本身都是数组,因此子数组也可以是矩形数组

int[][,] Arr = new int[3][,];
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 } };

比较矩形数组和交错数组

在CLI 中,一维数组有特定的性能优化指令。矩形数组没有这些指令,并且不在相同级别进行优化。因此,有时使用一维数组(可以被优化)的交错数组比矩形数组(不能被优化)更高效。

另一方面,矩形数组的编程复杂度要低得多,因为它会被作为一个单元而不是数组的数组

foreach 语句

foreach 语句就类似 C++ 新式的 for(:),或者说 python 的 for…in 的方式,允许我们连续的访问数组或者其他集合类型中的每一个元素

迭代变量是临时的,并且和数组中元素的类型相同。foreach语句使用迭代变量来相继表示数组中的每一个元素。

和 C++ 一样可以用 auto,C# 也可以在 foreach 中用 var 来使用隐式类型迭代变量声明

int[] arr1 = { 10, 11, 12, 13 };
foreach (int item in arr1)
    Console.WriteLine(item);

迭代变量是只读的

对于值类型,因为迭代变量是只读的,所以不能改变,而引用变量因为只保存了引用,虽然不能改变引用,但可以改变引用指向的数据

class MyClass // 注意 struct 也是值类型,换成 struct 的话,就无法过编译了
{
    public int MyField = 0;
}

class Program
{
    static void Main()
    {
        MyClass[] mcArray = new MyClass[4];
        for (int i = 0; i < mcArray.Length; i++)
        {
            mcArray[i] = new MyClass();
            mcArray[i].MyField = i;
        }

        foreach (var item in mcArray)
            item.MyField += 10;
        foreach (var item in mcArray)
            Console.WriteLine(item.MyField);
    }
}

foreach 语句和多维数组

矩形数组

矩形数组是当作一整片连续的元素遍历的

int total = 0;
int[,] arr1 = { { 10, 11 }, { 12, 13 } };
foreach (int element in arr1)
{
    total += element;
    Console.WriteLine($"Element: {element}, Current Total: {total}");
}
// Element: 10, Current Total: 10
// Element: 11, Current Total: 21
// Element: 12, Current Total: 33
// Element: 13, Current Total: 46
交错数组

交错数组是数组的数组,因此 foreach 时是每个维度独立的

int total = 0;
int[][] arr1 = new int[2][];
arr1[0] = new[] { 10, 11 };
arr1[1] = new[] { 12, 13, 14 };

foreach (int[] arr in arr1)
{
    Console.WriteLine("Starting new array");
    foreach (int i in arr)
    {
        total += i;
        Console.WriteLine($"Item: {i}, Current Total: {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

数组协变

某些情况下,即使某个对象不是数组的基类型,也可以把它赋值给数组元素。这种属性叫数组协变。在下面的情况下可以使用数组协变

  • 数组是引用类型数组
  • 在赋值的对象类型和数组基类型之间有隐式转换或显式转换

派生类和基类之间总是有隐式转换,因此总是可以将一个派生类的对象赋值给基类声明的数组

数组继承的有用成员

image-20250504195833398

值得注意的是交错数组的维度只有一维,它本质上是个一维数组,只不过每个元素都是个数组,Length 对于矩形数组获取的是所有元素的总数,而不是一个维度的数量

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

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

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

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

    Console.WriteLine();
    Console.WriteLine($"Rank = {arr.Rank}, Length = {arr.Length}");
    Console.WriteLine($"GetLength(0) = {arr.GetLength(0)}");
    Console.WriteLine($"GetType() = {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 方法

Clone 方法为数组进行浅复制

返回的是 object 类型的引用,它必须被强制转换成数组类型

数组与 ref 返回和 ref 局部变量

你可以利用 ref 返回,返回数组的某一个元素的引用,然后单独的修改这个元素。

委托

什么是委托

可以认为委托是持有一个或多个方法的对象。委托和典型的对象不同,可以执行委托,这时委托会执行它所持有的方法。类似于 C++ 的 std::function

delegate void MyDel(int value); // 声明委托类型

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

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

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

        MyDel del; // 声明委托变量

        // 创建随机整数生成器,并得到 0 到 99 之间的随机整数
        Random rand = new Random();
        int randomValue = rand.Next(99);

        del = randomValue < 50
            ? new MyDel(p.PrintLow) // 如果随机数小于 50,使用 PrintLow 方法
            : new MyDel(p.PrintHigh); // 否则使用 PrintHigh 方法

        del(randomValue);
    }
}
// 65 - High Value

委托概述

委托持有的方法可以来自任何类或结构,只要和委托的返回类型和签名(包括 ref 和 out 修饰符)匹配即可

调用列表中的方法可以是实例方法,也可以是静态方法

在调用委托时,会执行其调用列表中的所有方法

声明委托类型

基本和方法的声明完全一样,只是以 delegate 关键字开头,没有方法主体,且由于其是类型声明,因此不需要在类内部声明

创建委托对象

委托是引用类型,因此也有引用和对象。我们有两种方法,一种是传统的使用 new 运算符的对象创建表达式,如前面的例子。另一种就是快捷语法,直接将方法赋值给委托。方法名称和其相对应的委托类型之间存在隐式转换

del = p.PrintLow;

给委托赋值

和其他引用类型一样,旧的委托变量会被垃圾回收器回收

组合委托

委托可以使用额外的运算符来“组合”

MyDel delA = MyInstObj.MyM1;
MyDel delB = SClass.OtherM2;

MyDel delC = delA + delB;

委托是恒定的,委托对象被创建后不能再被更改,组合本质上是创建了一个新的委托

为委托添加方法

使用 += 运算符添加新的方法,新方法会在调用列表的底部,可以重复添加同一个方法的多个实例,重复的实例也会依次重复执行。

为委托移除方法

类似的使用 -= 运算符移除

  • 如果调用列表中的方法有多个实例,-= 运算符会从列表的最后开始搜索第一个匹配到的并移除。
  • 删除不存在的方法将无效。
  • 试图调用空的委托会抛出异常。判断空委托可以和 null 进行比较,调用列表为空时,委托为 null

调用委托

除了调用方法一样调用委托,还可以使用委托的 Invoke 方法

Medel delVar = inst.MyM1;
delVar += SCI.m3;
delVar += X.Act;
...
if(delVar != null)
    delVar(55);
delVar?.Invoke(65); // 使用 Invoke 和空条件运算符

调用带返回值的委托

当委托有返回值并且调用列表有一个以上的方法时,委托的返回值是调用列表中最后一个方法的返回值,其他方法的返回值会被忽略

调用带引用参数的委托

和连续调用那几个方法没区别,所有对引用参数的修改都会依次累计

匿名方法

有些一次性的方法,没必要创建具名的方法,因此有了匿名方法

delegate void MyDel(int value);

class Program
{
    static void Main()
    {
        MyDel del = delegate(int value) { Console.WriteLine(value); };
        del(42);
    }
}

使用匿名方法

我们可以在如下地方使用匿名方法

  • 声明委托变量时作为初始化表达式
  • 组合委托时在赋值语句的右边
  • 为委托增加事件时在赋值语句的右边

匿名委托的语法

  • delegate 类型关键字
  • 参数列表,如果没有任何参数可以省略
  • 语句块

参数和返回值必须和声明的委托类型相匹配,不过当委托的参数列表不包括任何 out 参数,且匿名方法内没有使用任何参数时,也可以省略参数列表或者圆括号内为空。

如果委托声明的参数列表中包含了 params 参数,那么匿名方法的参数列表必须省略 params 关键字

变量和参数的作用域

参数和内部的局部变量作用域显然还是在函数体内,但匿名方法有和 C++ lambda 表达式一样的捕获效果,可以访问到外部的变量,相当于 C++ 的引用捕获的效果。

delegate void MyDel();

class Program
{
    static void Main()
    {
        MyDel del;
        {
            int x = 5;
            del = delegate
            {
                x += 10;
                Console.WriteLine($"delegate: {x}");
            };
            del();
            Console.WriteLine(x);
        }
        del();
    }
}
// delegate: 15
// 15
// delegate: 25

Lambda表达式

C# 2.0 就引入了匿名方法,但是语法上还是有点冗余,C# 3.0 引入了真正的 Lambda 表达式来简化语法,用法上和 JavaScript 的箭头函数差不多。编译器通过Lambda 表达式可以知道更多的信息,因此我们可以省略更多的信息。在只有一个return 语句,只有一个参数时,可以连括号都省去

MyDel del = delegate(int x) { return x + 1; };
MyDel l1 = (int x) => { return x + 1; };
MyDel l2 = (x) => { return x + 1; };
MyDel l3 = x => { return x + 1; };
MyDel l4 = x => x + 1;

Lambda 表达式的参数不一定需要包含类型(隐式类型),除非委托有 ref 或者 out 参数——此时必须注明类型(显式类型)

如果没有参数就需要保留圆括号

事件

发布者和订阅者

很多程序都有一个共同的需求,即当一个特定的程序事件发生时,程序的其他部分可以得到该事件已发生的通知。

**发布者/订阅者模式(观察者模式)**可以满足这种需求。在这种模式下,发布者定义了一系列程序的其他部分可能感兴趣的事件。其他类可以“注册”,以便在这些事件发生时收到发布者的通知。订阅者通过向发布者提供一个方法来“注册”以获取通知。当事件发生时,发布者“触发事件”,然后执行订阅者提交的所有事件。

由订阅者提供的方法称为回调方法,还可以将它们称为事件处理程序

事件很多部分都和委托类似。实际上,事件就像是专门用于某种特殊用途的简单委托。事件其实就包含了一个私有的委托

  • 事件提供了对它的私有控制委托的结构化访问。也就是说,无法直接访问委托
  • 事件中可用的操作比委托少,对于事件我们只能添加、删除或调用事件处理程序
  • 事件触发时,它调用委托来依次调用调用列表中的方法

源代码组件概览

事件的使用总共由 5 部分

  • **委托类型声明:**事件和事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述
  • **事件处理程序声明:**订阅者类中会在事件触发时执行的方法声明。它们不一定是显式命名的方法,也可以是匿名方法或 Lambda 表达式
  • **事件声明:**发布者必须声明一个订阅者类可以注册的事件成员。当类声明的事件为 public 时,称为发布了事件
  • **事件注册:**订阅者必须注册事件才能在事件触发时得到通知。这是将事件处理程序与事件相连的代码
  • **触发事件的代码:**发布者类中“触发”事件并导致调用注册的所有事件处理程序的代码

声明事件

发布者类必须提供事件对象。创建事件比较简单——只需要委托类型和名称

  • 事件声明在一个类里
  • 它需要委托类型的名称
  • 它声明为 public(不是必须的)
  • 也可以是静态的
  • 可以在一个声明语句中声明多个事件
class Incrementer
{
    public event EventHandler MyEvent1, MyEvent2, OtherEvent;// EventHandler 是委托类型

需要注意事件是成员而不是一个类型,是类或结构的成员。被隐式自动初始化为 null

订阅事件

和委托添加方法一样,使用 += 运算符,因为不能赋值,所以先 + 在赋值是不行的。

触发事件

和委托执行的方式完全一样,方法调用的形式或使用 invoke() 方法,需要注意事件的触发只能由发布者类来完成,其他类只能订阅和取消订阅。

标准事件的用法

GUI 是事件处理驱动的,显然程序事件的异步处理是使用 C# 事件的绝佳场景。Windows GUI 编程广泛的使用了事件,因此对事件,.NET 框架提供了一个标准模式,该标准模式的基础就是 System 命名空间中声明的 EventHandler 委托类型。其声明如下代码所示

  • 第一个参数用来保存触发事件的对象的引用。由于是 object 类型的,所以可以匹配任何类型的实例
  • 第二个参数用来保存状态信息,指明什么类型适用于该应用程序
  • 返回类型是 void
public delegate void EventHandler(object sender, EventArgs e);

EventHandler 委托类型的第二个参数是 EventArgs 类的对象,它声明在System 命名空间中

  • EventArgs 不能传递任何数据。它用于不需要传递数据的事件处理程序——通常会被忽略
  • 如果你希望传递数据,必须声明一个派生自 EventArgs 的类,并使用合适的字段保存需要传递的数据

它提供了一个通用的签名,让所有事件都正好有两个参数

通过扩展 EventArgs 来传递数据

public class IncrementerEventArgs : EventArgs
{
    public int IterationCount { get; set; }
}

class Incrementer
{
    public event EventHandler<IncrementerEventArgs> CountedADozen; // 使用自定义类的泛型委托

    public void DoCount()
    {
        IncrementerEventArgs args = new IncrementerEventArgs();
        for (int i = 1; i < 100; i++)
            if (i % 12 == 0 && CountedADozen != null)
            {
                args.IterationCount = i;
                CountedADozen(this, args);
            }
    }
}

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

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

    void IncrementerDozensCount(object sender, IncrementerEventArgs e)
    {
        Console.WriteLine($"Incremented at Iteration {e.IterationCount} in {sender}");
        DozensCount++;
    }
}

class Program
{
    static void Main()
    {
        Incrementer incrementer = new Incrementer();
        Dozens dozens = new Dozens(incrementer);
        incrementer.DoCount();
        Console.WriteLine($"Dozens Count: {dozens.DozensCount}");
    }
}
// Incremented at Iteration 12 in Incrementer
// Incremented at Iteration 24 in Incrementer
// Incremented at Iteration 36 in Incrementer
// Incremented at Iteration 48 in Incrementer
// Incremented at Iteration 60 in Incrementer
// Incremented at Iteration 72 in Incrementer
// Incremented at Iteration 84 in Incrementer
// Incremented at Iteration 96 in Incrementer
// Dozens Count: 8

移除事件处理程序

同委托移除方法,不过只能 -=

事件访问器

之前提过,事件只能 += 和 -= 运算符。我们可以改变这两个运算符的行为来执行自定义的代码。

首先我们需要为事件定义事件访问器,有两个访问器 add 和 remove,声明方式类似属性

声明了事件访问器后,事件就不包括任何内嵌的委托对象了,必须自己实现

事件访问器表现为 void 方法,也就是不能使用有返回值的 return 语句