Covers:委托与事件,Lambda 是补充内容
我们刚刚学过泛型,委托与泛型有着很多联系,Lambda 又常常与委托配合使用,因此我们先讨论这一块。
现代的通用计算机在架构上属于冯 · 诺依曼机,它是由美籍匈牙利科学家约翰 · 冯 · 诺依曼等人提出的计算机架构。世界上第一台冯氏机,即“原型机”是 1949 年的 EDVAC。冯氏机机要求计算机上的 一切数据,以及对数据进行的一切操作,都是可存储的;因此,冯氏机是一种以存储为中心的计算机架构。
看了上面的科普,你会发现:原来“对数据进行的操作”也是可存储的,这意味着 程序和数据,在物理地位上是等同的,在程序运行过程中,它们 都存储在计算机的 “内存”这个高速 存储器中。
那么,既然可以通过编程来操作各种数据,是否也可以像操作数据那样直接操作函数呢?C 和 C++ 中,我们用函数指针操作其他函数,函数指针也是指针(若不了解指针,请把它想象成 C# 中的引用),它指向函数,其实就是指向了函数编译后存储在内存中的位置,可以对函数指针使用函数调用运算符 ()
来调用它指向的函数;Java 中并没有任何类似的特性。
C# 引入了 委托 的概念,它对应着 C/C++ 中的函数指针,也是一种特殊的类,委托对象能够保存其它方法,并在需要时调用。
委托与类的地位一样,因此我们 要在命名空间作用域中定义它(常见的错误是将其定义在类内),语法为 ACCESS delegate RET_TYPE NAME(PARAMS);
:
我们写一个小例子:
using System;
namespace LearnDelegate
{
public delegate void SimpleOperation();
class Program
{
static void Hello()
{
Console.WriteLine("Hello, delegate!");
}
public static void Main()
{
SimpleOperation f = Hello;
f();
}
}
}
我们定义了委托类型 SimpleOperation,它可以表示任何无返回值、不接受参数的方法;在 Program 中我们定义了一个方法 Hello,它的签名符合 SimpleOperation 的要求;在 Main 中我们先产生一个 SimpleOperation 的对象“f”(注意它可以不用 new 关键字),并为其赋予方法“Hello”(这里的方法名后面不能加括号,加上就是调用 Hello 了);接下来有趣的事情出现了:我们 可以直接“调用”委托对象 f,好像它是一个方法一样。
学习了泛型之后,我们就会想用泛型来扩大一个定义的适用范围。委托经常与泛型联用:
using System;
namespace LearnDelegate
{
public delegate T BinaryOperation<T>(T l, T r);
static class Math
{
public static double Add(double l, double r) => l + r;
public static double Subtract(double l, double r) => l - r;
public static double Multiply(double l, double r) => l * r;
public static double Divide(double l, double r) => l * (1.0 / r);
public static double Power(double l, double r) => System.Math.Pow(l, r);
}
class Program
{
static void Main()
{
BinaryOperation<double> calculator = Math.Power;
Console.WriteLine(calculator(2, 10));
}
}
}
我们定义了“二元操作”泛型委托类型 BinaryOperation,它能够代表任何“接受两个同型参数,返回同型值”的方法,且类型不受限制;接着我们定义了 Math 类,它封装了一些简单快乐的算术方法(System.Math.Pow 是求乘方的内置方法,这样就不用手写这个函数了);在测试代码中我们赋予 T 以 double 类型,从而产生了一个委托对象“calculator”,它就像一个功能未定的计算器,我们赋予它乘方的功能,并调用了它。
委托类本身是没有定义体的,说到底它们只是一些对函数签名的约定;如果我们要大量使用委托,一个个定义或许令人感到机械和低效。.NET 为我们写好了两大类泛型委托 Func 和 Action,它们跟我们手写的委托用了完全一样的语法,使用上完全没有区别,因此 大多数情况下我们是不需要自己手写委托的:
Func
对应接受 2 个 int 型参数,返回 double 型的方法。你问这种泛型可变形参表是用了什么技巧?额,微软其实 逐个 定义了它们,不过 32 个而已。
用 Func 委托改写上面“计算器”例子的测试代码:
static void Main()
{
Func<double, double, double> calculator = Math.Power;
Console.WriteLine(calculator(2, 10));
}
如果很长的类型参数列表让你感到丑陋,可以用我们的朋友 using
关键字来给某一种 Func 起别名:
using System;
namespace LearnDelegate
{
using BiOp = Func<double, double, double>;
class Math ...
class Program
{
static void Main()
{
BiOp calculator = Math.Power;
Console.WriteLine(calculator(2, 10));
}
}
}
注意:using 语句只能出现在命名空间的开头,或整个文件的开头。
C# 的委托提供了 多播 特性,一个委托可以按顺序执行一系列方法。使用多播的语法简单得难以置信,只要给委托对象做加法即可:
...
Action dele = Method1;
dele += Method2;
...
在执行 dele 时,就会依次调用 Method1、Method2……调用的顺序就是你将方法加给 dele 的顺序。
委托可以派生线程,在其他线程中执行,这是 C# 常用的并发方式之一。由于这里涉及到并发编程知识,展开讲太费笔墨,因此这一节仅供有基础的朋友作简单参考。
BeginInvoke
方法,令其派生线程并在其中执行委托所绑定的方法,BeginInvoke 接受两个参数,第一个是线程结束后的回调,不需要时给 null
即可;第二个是非静态方法的主调对象,也可以给 null
;EndInvoke
方法等待派生出的线程返回,要使用该方法,必须获得 BeginInvoke 方法返回的 IAsyncResult 对象,将其作为 EndInvoke 的唯一参数传入。下面的例子演示了 BeginInvoke 和 EndInvoke 的使用,引入的 Threading 命名空间仅用于线程睡眠,非必须:
using System;
using System.Threading;
namespace LearnAsyncDelegate
{
class Actor
{
string _name;
public Actor(string name)
{
_name = name;
}
public void Run()
{
Console.WriteLine(_name + ".Run called");
// Sleep for a period of time:
Thread.Sleep(200);
Console.WriteLine(_name + ".Run returning");
}
}
class Program
{
static void Main()
{
// All objects:
Actor[] staff =
{
new Actor("#1"),
new Actor("#2"),
new Actor("#3"),
new Actor("#4"),
new Actor("#5"),
};
// All delegates:
Action[] roles =
{
staff[0].Run,
staff[1].Run,
staff[2].Run,
staff[3].Run,
staff[4].Run,
};
// A pre-allocated IAsyncResult array:
var results = new IAsyncResult[5];
// Begin threads:
for (int i = 0; i < roles.Length; ++i)
{
results[i] = roles[i].BeginInvoke(null, null);
}
// Wait for threads to join:
for (int i = 0; i < roles.Length; ++i)
{
roles[i].EndInvoke(results[i]);
}
}
}
}
可能的控制台输出:
#1.Run called
#2.Run called
#5.Run called
#3.Run called
#4.Run called
#2.Run returning
#4.Run returning
#3.Run returning
#5.Run returning
#1.Run returning
Lambda(常称为 Lambda 表达式)是 匿名函数,它是函数,但它没有名字,但我们通过学习委托已经知道,函数其实与数据一样,都可以被程序所处理。我们可以将 Lambda 赋予委托对象,再通过委托调用之。
Lambda 的定义语法是:
(PARAMS)=>{BODY}
,即 用括号包围的形参表与函数执行体之间用一个 =>
连接;=> RET_VAL
的语法,省去 BODY 直接返回 RET_VAL;使用 Lambda 表达式可以避免定义很多只在一个作用域中使用的细碎方法,我们写一个例子:
using System;
namespace LearnDelegate
{
using BiOp = Func<double, double, double>;
class Program
{
static void Main()
{
BiOp add = (l, r) => l + r;
BiOp subtract = (l, r) => l - r;
BiOp multiply = (l, r) => l * r;
BiOp divide = (l, r) => l * (1.0 / r);
Console.WriteLine("3.0 + 7.0 = " + add(3.0, 7.0));
Console.WriteLine("3.0 - 7.0 = " + subtract(3.0, 7.0));
Console.WriteLine("3.0 * 7.0 = " + multiply(3.0, 7.0));
Console.WriteLine("3.0 / 7.0 = " + divide(3.0, 7.0));
}
}
}
上面的程序没有定义任何细碎的小方法,命名空间比较干净。
上面的例子中,绑定了 Lambda 的委托对象就像一个“可以调用的局部变量”一样;委托作为一种对象,也要占用一部分资源,那我们是否可以抛开委托,直接把函数像一个局部变量那样定义出来,同时也能用完就扔呢?
局部函数 是函数,因为它可以被调用;它又是局部的,因为它只在其作用域内有效。我们用 RET_TYPE FUNCTION(PARAMS) => {BODY}
来定义局部函数,它可以直接定义在程序的执行体中,除了能够被调用,它就像一个局部变量。
局部函数不再是匿名的(必须为其命名),因为如果它也没有名字,就再也没什么符号可以指代这个函数了;局部函数也可以被绑定到委托上。
继续改写上面的例子:
static void Main()
{
double Add(double l, double r) => l + r;
double Subtract(double l, double r) => l - r;
double Multiply(double l, double r) => l * r;
double Divide(double l, double r) => l * (1.0 / r);
Console.WriteLine("3.0 + 7.0 = " + Add(3.0, 7.0));
Console.WriteLine("3.0 - 7.0 = " + Subtract(3.0, 7.0));
Console.WriteLine("3.0 * 7.0 = " + Multiply(3.0, 7.0));
Console.WriteLine("3.0 / 7.0 = " + Divide(3.0, 7.0));
}
注意,我们抛开了委托的概念(这段代码里没有用到委托),因此函数的形参类型就无法从委托的签名要求中推断了,因此不能省略。
T.B.C.