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
,所以 new
和 override
修饰符是可以使用的,可以用于覆盖或重写一些基类的成员。
结构作为返回值和参数
和其他值类型没啥区别
关于结构的更多内容
因为是值类型,且限制较多,所以开销也比类小,用结构替代类有时可以提高性能,但需要注意装箱和拆箱的高昂代价
- 预定义简单类型(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
位标记
程序员长期使用单个字的不同位作为表示一组开/关标志的紧凑方法,将其称为标志字。枚举提供了简单实现它的简便方法
一般步骤如下
- 确定需要多少个标志位,并选择一种足够多位的无符号类型来保存它
- 确定每个位置代表什么,并给它们一个名称。声明一个选中的整数类型枚举,每个成员由一个位位置表示
- 使用按位或运算符在持有该位标志的字中设置适当的位
- 使用按位与运算符或
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
数组协变
某些情况下,即使某个对象不是数组的基类型,也可以把它赋值给数组元素。这种属性叫数组协变。在下面的情况下可以使用数组协变
- 数组是引用类型数组
- 在赋值的对象类型和数组基类型之间有隐式转换或显式转换
派生类和基类之间总是有隐式转换,因此总是可以将一个派生类的对象赋值给基类声明的数组
数组继承的有用成员
值得注意的是交错数组的维度只有一维,它本质上是个一维数组,只不过每个元素都是个数组,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 语句