C#高级编程第11版

【1】8.1 引用方法

1.委托是指向方法的.NET地址变量。

2.委托是类型安全的类,定义了返回类型和参数类型。委托类不单单只包含一个方法引用,它也可以保存多个方法的引用。

3.Lambda表达式直接跟委托相关。当参数是类型是一种委托类型,你可以使用Lambda表达式来实现一个被委托引用的方法。

【2】8.2 委托

方法在调用前,它需要被关联成某个类的实例。因此不允许直接访问方法地址。而当你需要这么处理的时候,你可以将方法细节封装成一种新的对象:委托。

委托则是存储了一个方法或者多个方法的访问地址。

【3】8.2.1 声明委托

1.声明委托的几种定义:

这里我们定义了一个叫IntMethodInvoker的委托,并且指定了这个委托的每个实例,可以保存(hold)指向返回值为void并接收一个int类型参数的方法引用。委托最重要的一点是,它是类型安全的。当你定义委托的时候,你提供了完整的方法细节,包括方法的签名以及它的返回值。

delegate void IntMethodInvoker(int x);

假定你想定义一个委托,叫做TwoLongOp,用来指代(represents)一个带有两个long类型参数并返回double类型的方法。你可以像这么做:

delegate double TwoLongsOp(long first, long second);

定义一个不接受任何参数仅仅返回string类型的方法委托,你可以像这么写:

delegate string GetAString();

2.可以像修饰普通class那样给委托加上访问修饰符——public,private,protected等等,如下所示:

public delegate string GetAString();

3.定义一个委托真的意味着定义了一个新的类。委托类实际上派生自System.MulticastDelegate,而它又派生自基类System.Delegate。C#编译器清楚委托类的存在并使用委托语法隐藏了委托类的操作细节。这也是另外一个可以用来说明C#是如何通过与基类联动并尽可能地让编程变得简单的例子。

4.当你定义完一个委托之后,你可以创建它的实例对象,并且用创建的实例来保存特定方法的细节。

5.当我们讨论委托的时候,它只有一个术语。委托既可以用来代表委托类,也可以用来指代具体的委托实例。当你创建一个委托实例的时候,它本身也可能作为一个委托被引用(is also referred to as a delegate)。你需要根据上下文来理清我们说到的委托究竟代表何种含义。

【4】8.2.2 使用委托

1.支持委托实例直接通过小括号进行调用,跟你调用实例里的Invoke方法是一样的。

2.委托推断:在每个应用到委托实例的地方,你都可以只敲方法名。也可省略这个new部分的代码,仅仅将方法名传递给委托变量。委托推断可以在任何用到委托变量的地方生效。

3.x.ToString作为参数传递给构造函数。在这里你不能在ToString后面敲上小括号变成"x.ToString()"这样子。因为这意味着一次方法调用,这个方法调用返回的是一个string类型的对象,而这种类型无法赋值给一个委托类型的变量。你只能将方法的地址赋值给委托变量。

4.给定委托的实例可以引用任意类型任意对象上的实例或者静态方法,只要方法签名跟委托需要的方法签名一致即可。

5.GetAString()仅仅是个委托名,用于下面来调用,()里代表要调用哪个方法的参数。

【5】8.2.3 简单的委托示例

1.一种使用委托的方式——将相关方法组织到一个数组中,然后你就可以通过循环来重复调用它们。

2.上述代码中关键的一行是你将哪个委托传递给ProcessAndDisplayNumber方法,譬如这样:

ProcessAndDisplayNumber(operations[i], 2.0);

之前的例子仅仅只是将委托的名称进行传递,并不包含任何参数,而在这里,通过将operations[i]作为参数进行传递,语法上其实做了两步处理:

  • operations[i]意味着一个委托,它引用了某个方法;
  • operations[i](2.0)意味着将2.0作为参数传递给委托引用的方法,进行一次方法调用。

【6】8.2.4 Action<T>和Func<T>委托

1.除了为每个参数和返回类型定义一个新的委托,你也可以直接使用Action<T>和Func<T>委托。

2.参考:https://www.cnblogs.com/asdyzh/p/9794451.html

3.当委托返回类型为void的时候,你可以使用预定义的Action<T>委托。

【7】8.2.5 BubbleSorter 示例

1.冒泡排序算法是一个常见算法并且可以非常简单地对数字进行排序。它对于小集合的数字处理非常好用,因为对于大集合的数字(譬如10个以上),有其他的更有效率的算法。

2.通过使用泛型版本的Sort<T>方法,来实现不是int类型的比较。

3.comparison委托引用的方法,必须带有两个同类型的参数,并且当第一个参数值小于第二个的时候,将返回true,否则返回false。

static public void Sort<T>(IList<T> sortArray, Func<T, T, bool> comparison)

4.注意为了和Func<T, T, bool>委托的签名匹配,你在这个类里定义的CompareSalary必须包含两个Employee参数,并且返回一个布尔值。

public static bool CompareSalary(Employee e1, Employee e2) => e1.Salary < e2.Salary;

从小到大排列。

【8】8.2.6 多播委托

1.封装多个方法的这种委托我们称之为多播委托(multicast delegate)。当你调用多播委托的时候,它会按顺序调用它引用的所有的方法。为了使多播委托有效,委托的返回值必须为void,否则,你只能得到委托引用的最后一个方法的返回值。

2.多播委托:+=,-=。+=替代了数组。

3.在引擎下,一个多播委托实际上是一个派生自System.MulticastDelegate的类,而MulicastDelegate又派生自System.Delegate。System.MulticastDelegate拥有额外的成员,允许将方法链的调用存储到一个列表中

4.当你使用多播委托时,请记住同一个委托引用的方法链中的顺序形式上是未定义的(formally undefined)。因此,请尽量不要编写一些需要依赖于特定执行顺序的方法。

5.通过一个委托来调用多个方法可能会导致一个更大的问题。多播委托包含了一个委托集合并挨个进行调用。假如其中某个方法抛出了一个异常,那么后续的调用就会中止。

在这种场景下,你也可以通过自己枚举委托的方法列表,来避免这种问题的发生。委托类里定义了一个方法,GetInvocationList,用来返回一个Delegate的数组对象。现在你可以通过它,直接调用它们引用的方法,捕获异常,并执行下一个方法:

Delegate[] delegates = d1.GetInvocationList();
    foreach (Action d in delegates)
    {
        try
        {
            d();
        }
        catch (Exception)
        {
            Console.WriteLine("Exception caught");
        }
    }

先枚举委托的方法列表,然后遍历。

【9】8.2.7 匿名方法

1.委托要起效的话,它必须指向某个已经存在的方法,这意味着,委托必须跟它想要引用的方法拥有相同的签名

2.匿名方法是一段代码用来作为委托的参数。

3.没有将某个具体的方法名称赋值给变量,取而代之的是,我们使用了一个简单的代码块,通过delegate关键字以及紧随其后的string参数声明来赋值。

4.当你为某个事件定义委托的时候,这点将会变得更加明显(evident),它使得你不用书写更多复杂代码,尤其当你需要定义大量事件处理函数时。使用匿名方法并不会让你的代码跑的快些,编译器依然将其当成一个正常的方法,只不过在后台为其自动生成一个你不知道的方法名而已。

5.

在使用匿名方法时你必须遵守一系列的规则:

  • 你不能在匿名方法中使用任何跳转语句,例如break,goto或者continue。反过来也一样,外部的跳转语句不能跳转到匿名方法中去。
  • 匿名方法中不能使用不安全的代码(Unsafe code),并且匿名方法外定义的ref或者out参数无法访问,但是其他定义在匿名方法外的变量可以引用。
  • 假如同样的功能需要被多次使用,就别用匿名方法了。在这种情况下,与其反复书写匿名方法,还不如定义一个正常的命名方法进行复用呢。

6.在新版本的程序中你不再需要使用这个语法,因为lambda表达式提供了同样并且更丰富的功能。

【10】8.3 lambda 表达式

在lambda表达式运算符=>的左边,列出了所需的参数,而运算符右边则定义了方法实现,并最终将其赋值给委托变量lambda。

【11】8.3.1 参数

1.假如只有一个参数:

Func<string, string> oneParam = s => $"change uppercase {s.ToUpper()}";
Console.WriteLine(oneParam("test"));

2.假如委托需要多个参数:

Func<double, double, double> twoParams = (x, y) => x * y;
Console.WriteLine(twoParams(3, 2));

或者:

Func<double, double, double> twoParamsWithTypes = (double x, double y) => x * y;
Console.WriteLine(twoParamsWithTypes(4, 2));

【12】8.3.2 多行代码

1.假如lambda表达式仅仅只包含一行语句,那么方法块的左右大括号和return语句都可以省略,编译器会为你隐式地添加return:

Func<double, double> square = x => x * x;

写全:

Func<double, double> square = x =>
{
    return x * x;
};

2.当你的方法实现需要用到多行代码的时候,方法块的{}和显式的return关键字都是必须的。尽量写出来。

【13】8.3.3 闭包

1.在lambda表达式里你也可以访问代码块外的变量,这种方式被称之为闭包(closure)。小心使用。

2.为了实现lambda表达式x => x + someVal,编译器创建了一个匿名类,这个匿名类里包含了私有变量someVal,并且拥有一个构造函数来传递这个外部变量。这个构造函数会根据你将会访问多少外部变量而生成。

使用lambda表达式并且调用该方法的时候,创建了一个匿名类的实例并且当方法被调用的时候,将局部变量的值传递给匿名类的实例。

3.当你在lambda表达式里修改了一个闭包变量的值时,你在外部访问这个变量的时候,得到的也是修改后的值。

4.当你在多线程使用闭包的时候,你可能会陷入并发冲突。因此在使用闭包时,最好只使用不可变类型。这样做能够保证值是永久不变的,并且不需要同步处理。

你可以在任何委托类型的地方使用lambda表达式,而另外一个可以使用lambda表达式的类型是Expression或者Expression<T>,编译器用它来创建表达式树。

【14】8.4 事件

1.事件是基于委托的,并且对委托提供了发布/订阅的机制。

【15】8.4.1 事件发布程序

1.空条件运算符?.在C# 6.0版本开始支持。事件的处理,对处理器进行空值处理:

if (NewCarInfo != null)
{
    NewCarInfo(this, new CarInfoEventArgs(car));
}//C#6.0以前

NewCarInfo?.Invoke(this, new CarInfoEventArgs(car));

记住在触发事件之前,实现检查委托是否为null是很有必要的,假如没有任何人订阅的话,委托的值就是null,此时调用其方法将会引发异常。

2.EventHandler<TEventArgs>定义了一个处理器,接收两个参数并返回void,第一个参数是object类型,而第二个参数是泛型TEventArgs类型。

3.EventHandler<TEventArgs>的定义如下,其中限定了泛型类型必须是EventArgs类或者其派生类:

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs: EventArgs

4.在C#里,通过event关键字你可以在一行里声明EventHandler,编译器在后台为它创建了对应的委托类型变量,并且添加了add和remove关键字来订阅/退订委托。它与自动完成属性以及完整属性非常类似,完整的声明语法如下所示:

private EventHandler<CarInfoEventArgs> _newCarInfo;
public event EventHandler<CarInfoEventArgs> NewCarInfo
{
    add => _newCarInfo += value;
    remove => _newCarInfo -= value;
}

5.完整版的事件定义也是很有用的,假如你需要在其中添加其他的逻辑的话,譬如在多线程访问中添加同步代码。UWP和WPF控件有时候也通过完整定义来添加事件的冒泡和隧道处理(bubbling and tunneling functionality with the events)。

6.CarDealer类通过调用委托的Invoke方法来触发事件。这将会调用所有订阅了该事件的处理器。记住,就像前面多播委托演示的那样,方法调用的顺序是没有保障的。为了能更好地控制多有处理器的执行,你可以使用Delegate类里的GetInvocationList方法来访问委托里的所有项并且对每项单独调用,就像之前演示的那样。

【16】8.4.2 事件侦听器

通过定义一个类来充当事件监听器。当这个类订阅了某事件,则可以接收到设定好的信息,如果退订则就接收不到了。通过+=事件名订阅,-=事件名退订。

【17】8.5 小结

本章介绍了委托,lambda表达式和事件的基础知识。你已经学到了如何声明一个委托,并如何将方法添加到委托队列里。你也了解到如何使用lambda表达式来实现委托的方法调用,并且你还了解了为一个事件声明处理器的过程,包括如何创建一个自定义的事件已经如何触发这个事件。

在设计大型应用程序时使用委托和事件能大大降低各层之间的依赖关系。这使得你能开发高复用的程序组件。

lambda表达式是C#的语言特性,它是基于委托的,通过它,你可以省略不少代码。lambda表达式不单只用在委托上,它还能用在LINQ中。

参考网址:https://www.cnblogs.com/zenronphy/p/ProfessionalCSharp7Chapter8.html#81-%E5%BC%95%E7%94%A8%E6%96%B9%E6%B3%95

原文地址:https://www.cnblogs.com/wazz/p/14158540.html