委托
委托是方法的类型。
有了类型就可以声明方法的变量,参数,字段。然后再调用他。
很多新人很困惑,为什么要把方法做成变量,不直接去调用它呢?
这是因为在目前你的认知里,代码都是你一个人写出来的。
如果你的组长把任务分给了4位同学,你还能时刻监察其他人的代码吗?
如果你懒得自己写代码,找了一份扩展包呢?
还有c#自带的Console.WriteLine
,这些代码也不是你写的。
他们写这些代码的时候,还不知道你写了什么方法,那要怎么在他们的代码里调用你的方法呢?
变量和参数,最开始就是用来解决编写时无法确定的东西。
委托是方法的类型,有了委托就能定义方法的变量。
Action
Action委托是返回void的委托。
他的各泛型版本对应了方法的参数类型。
Action<int> cw = Console.WriteLine;//用方法给委托赋值时不要带上方法调用的括号
cw(22);//调用委托就像调用那个方法一样
Func
Func委托是有返回值的委托。它只有泛型的版本。
它的最后一个泛型代表方法的返回值类型。前面的泛型是方法的参数类型。
Func<string, bool> join = string.IsNullOrEmpty;
bool empty = join(null);
自定义委托
预置的Action和Func委托类型以及他们的各泛型版本,在大多数情况下都能满足我们使用。
但一些情况下我们仍然必须创建自己的委托类型,例如参数中具有ref
,out
,in
的时候。
委托是类型,是和类,结构,接口同级别的东西,所以委托可以直接定义在命名空间下。
委托类型使用delegate
关键字声明,后面跟随一个方法签名,不能带有实现。
delegate bool TryParse<T>(string s, out T t);
TryParse<int> tryParse = int.TryParse;
bool parse = tryParse("123", out int t);
多播委托
在给委托类型的变量赋值时,方法不能带有括号。
带括号和参数的是调用方法,使用的值就是方法的返回值。
除非你的方法会返回一个委托。用来给委托进行赋值的,称为方法组。
就像所有数组有共同基类
Array
,所有枚举有共同基类Enum
,所有结构有共同基类ValueType
。
所有的委托类型的共同基类是Delegate
。所以Action
和Func
以及他们所有泛型版本的类型都叫委托类型。
委托也可以和方法组进行+
和-
操作,但方法组和方法组不能。
当一个委托用+
储存了多个方法时,这种情况称为多播委托
多播委托会记录下所有+
过的方法组。在调用时会按照+
时候的顺序依次调用。
但有返回值的委托,只有最后一个绑定方法组的返回值会获得。
可以一个多播委托,可以适用-
来解绑方法组。
多播委托会在它记录的列表里查找需要解绑的方法组。
在找到匹配的方法组的时候,会解绑遇到的第一个匹配项。
如果找不到,那就无事发生,不会报错。
委托是引用类型
委托是引用类型,可以使用null
作为值。
并且,在+
和-
的过程中,遇到了null
或减完以后自己变成了null
也不会报错。
但对一个null
委托进行调用还是会报错的。
Action<int>? cw = Console.WriteLine;
cw -= Console.WriteLine;
cw += null;
Console.WriteLine(cw == null);
/*if (cw != null)
{
cw(20);
}*/
cw?.Invoke(20);//使用空传播代替if来终止委托调用。
//委托是一种类型,他有方法。Invoke就是调用这个委托。
匿名方法
在使用委托时,如果只是临时的使用一个方法,不想完整的写一遍方法的定义,可以使用匿名方法。
匿名方法只能在声明变量的时候使用,所以不能代替你直属类的方法的定义。
一个完整的匿名方法声明如下
Action<int>? _ =
void (int i) =>
{
Console.WriteLine(i * i);
};
和普通的方法声明相比,它没有名字(匿名),并且多了一个=>
。
一个匿名方法在以下条件时可以进行一些省略
- 如果用作给参数或变量赋值,且变量用了泛型能推测出匿名方法中参数和返回值的类型,
那么参数类型和返回值类型可以不写。 - 在参数和返回值类型省略时,如果同时参数正好是1个,那么可以省略参数的括号。
- 如果方法体内只有一条语句,那么可以省略方法体的大括号,和单语句的
;
。
如果一个方法需要Action<int>
类型的委托,使用匿名方法可以这样传入参数:
Hello(i => Console.WriteLine(i * i));
void Hello(Action<int> action)
{
action?.Invoke(20);
}
多个参数但省略类型就像这样
Hello2((a, b) => $"{a * b}+{a + b}");
void Hello2(Func<int, int, string> func)
{
Console.WriteLine(func(20, 30));
}
捕获变量
使用匿名方法或局部方法时,可以直接使用局部变量。
这种情况下,这个方法会显示捕获了变量。
这种情况下,他们获得的不是原始变量的复制,而是他们的引用。
也就是说,在方法内对捕获变量进行修改,也能作用到原来的变量上。
int a2 = 10;
int b2 = 20;
Action hello2 = () =>
{
a2 = b2;
};
Console.WriteLine(a2);//10
hello2();
Console.WriteLine(a2);//20
b2 = 60;
hello2();
Console.WriteLine(a2);//60
不仅修改值会作用到原来的变量上,获取值也会得到实时的值而不是创建时的值。
Action? For = null;
for (int i = 0; i < 10; i++)
{
For += () => Console.WriteLine(i);
}
For?.Invoke();//会得到10个10,而不是从0到9
捕获变量甚至可以让局部变量脱离它原本的作用域。
例如这里的临时迭代变量i
,本来只应该在for
循环内部存在。
因为捕获变量,把他带出来了。
事件
事件类似于属性,是一种带有访问器的成员。
事件的两个访问器是add
和remove
,分别在对事件使用+=
和-=
时触发。
当使用委托类型作为字段,使用属性的get
和set
限制访问几乎没有意义。
有了get
自己的委托就可能被他人随意调用。
有了set
就会被他人影响到自己调用委托。
事件在委托类型前加event
进行声明,然后像属性一样给他写两个访问器。
这两个访问器都是有一个类型和事件类型一样的参数,无返回值的方法。
class MyClass
{
public event Action? Action
{
add
{
action += value;
value?.Invoke();
}
remove
{
value?.Invoke();
action -= value;
}
}
private Action? action;
}
通常情况下事件仅允许出现在+=
或-=
的左边。
这样就阻止了别人调用这个委托,或者直接设置为null
。
委托是引用类型,如果要进行解绑必须要拿到原本的那个实例。
- 成员方法和局部方法都是静态的,他们都只有一份实例。
- 使用成员方法进行解绑,意味着你需要具有这个方法的访问权限。
- 局部方法只能在定义方法的内容使用,所以只有使用者能进行解绑。
- 但是匿名方法是动态创建的,每一次声明一个匿名方法都是不同的实例。
所以除非使用变量储存了这个匿名方法,否则没人能对他进行解绑。
自动实现的事件
像属性一样,如果你省略掉访问器的具体逻辑,
那么编译器会自动帮你添加一个匿名字段,
并为这个事件添加控制这个匿名字段的逻辑。
但事件的访问器不是可选的,这两个访问器都必须存在。
所以,省略访问器逻辑是连声明访问器的括号都不用写。
class MyClass2
{
public event Action? Action;
public void Invoke()
{
Action?.Invoke();
}
}
属性的访问和赋值以及其他使用,跟直接使用变量是一样的。
所以自动实现的属性和普通字段使用起来感觉没有什么差别。
但是事件只能出现在+=
或-=
前面。而委托可以像完整的变量一样进行访问。
所以,如果自动实现的事件仍然只能使用+=
或-=
,那就无法调用到这个匿名委托字段了。
满意以下条件时,可以把事件当作委托直接进行访问。
- 必须是有匿名委托字段的。这要求它必须是一个自动实现的事件。
- 必须是能存在字段的,这要求它不是接口的实例事件。
- 必须是能定义访问器逻辑的,这要求它不是抽象的事件。
- 只有定义类才能访问,即便派生类或外部具有这个事件访问权限。