当要把方法传送给其他方法时,就需要使用委托:。
声明委托的语法如下:
delegate void IntMethodlnvoker(int x);
在这个示例中,声明了一个委托IntMethodlnvoker,并指定该委托的每个实例都可以包含一个方法的引用,
该方法带有一个int参数,并返回voido理解委托的一个要点是它们的类型安全性非常高。在定义委托时,必
须给出它所表示的方法的签名和返回类型等全部细节。
下面的代码段说明了如何使用委托。这是在int值上调用TbString()方法的一种相当冗长的方式
class Program
{
private delegate string GetAString();
static void Main()
{
int x = 40;
GetAString firstStringMethod = new GetAString(x.ToString);
Console.WriteLine($"String is {firstStringMethod()}");
}
}
在这段代码中,实例化类型为GetAString的委托,并对它进行初始化,使其引用整型变量x的ToString()方法。在C#中,委托在语法上总是接受一个参数的构造函数,这个参数就是委托引用的方法。这个方法必须匹
配最初定义委托时的签名。
class MathOperations
{
public static double MultiplyByTwo(double value) => value * 2;
public static double Square(double value) => value * value;
}
delegate double DoubleOp(double x);
class Program
{
static void Main()
{
DoubleOp[] operations =
{
MathOperations.MultiplyByTwo,
MathOperations.Square
};
for (int i = 0; i < operations.Length; i++)
{
Console.WriteLine($"Using operations[{i}]:");
ProcessAndDisplayNumber(operations[i], 2.0);
ProcessAndDisplayNumber(operations[i], 7.94);
ProcessAndDisplayNumber(operations[i], 1.414);
Console.WriteLine();
}
}
static void ProcessAndDisplayNumber(DoubleOp action, double value) =>
Console.WriteLine($"Value is {value}, result of operation is {action(value)}");
}
在这段代码中,实例化了一个DoubleOp委托的数组。该数组的每个元素都初始化为 指向由MathOperations类实现的不同操作。然后遍历这个数组,把每个操作应用到3个不同的值上。这说明了 使用委托的一种方式一把方法组合到一个数组中来使用,这样就可以在循环中调用不同的方法了。
这段代码的关键一行是把每个委托实际传递给ProcessAndDisplayNumber方法,例如:
ProcessAndDisplayNumber(operations[i], 2.0);
其中传递了委托名,但不带任何参数。假定operations[i]是一个委托,在语法上:
• operations[i]表示“这个委托”。换言之,就是委托表示的方法。
• operationsi表示“实际上调用这个方法,参数放在圆括号中”。
ProcessAndDisplayNumber方法定义为把一个委托作为其第一个参数:
static void ProcessAndDisplayNumber(DoubleOp action, double value)
然后,在这个方法中,调用:
double result = action(value);
这实际上是调用action委托实例封装的方法,其返回结果存储在result中。运行这个示例,得到如下所示 的结果:
SimpleDelegate
Using operations[0]:
Value is 2, result of operation is 4
Value is 7.94, result of operation is 15.88
Value is 1.414, result of operation is 2.828
Using operations[1]:
Value is 2, result of operation is 4
Value is 7.94, result of operation is 63.0436
Value is 1.414, result of operation is 1.999396
Action委托表示引用一个void返回类型的方法。这个委托类存在不同的变体,可以传递至多16种不同的参 数类型。没有泛型参数的Action类可调用没有参数的方法。Actioni3用带一个参数的方法,Action
Func委托可以以类似的方式使用。Func允许调用带返回类型的方法。与Action类似,Func 也定义了不同的变体,至多也可以传递16个参数类型和一个返回类型。Func委托类型可以调用带 返回类型且无参数的方法,Func
示例:
class Employee
{
public Employee(string name, decimal salary)
{
Name = name;
Salary = salary;
}
public string Name { get; }
public decimal Salary { get; }
public override string ToString() => $"{Name}, {Salary:C}";
public static bool CompareSalary(Employee e1, Employee e2) =>
e1.Salary < e2.Salary;
}
class BubbleSorter
{
static public void Sort<T>(IList<T> sortArray, Func<T, T, bool> comparison)
{
bool swapped = true;
do
{
swapped = false;
for (int i = 0; i < sortArray.Count - 1; i++)
{
if (comparison(sortArray[i + 1], sortArray[i]))
{
T temp = sortArray[i];
sortArray[i] = sortArray[i + 1];
sortArray[i + 1] = temp;
swapped = true;
}
}
} while (swapped);
}
}
class Program
{
static void Main()
{
Employee[] employees =
{
new Employee("Bugs Bunny", 20000),
new Employee("Elmer Fudd", 10000),
new Employee("Daffy Duck", 25000),
new Employee("Wile Coyote", 1000000.38m),
new Employee("Foghorn Leghorn", 23000),
new Employee("RoadRunner", 50000)
};
BubbleSorter.Sort(employees, Employee.CompareSalary);
foreach (var employee in employees)
{
Console.WriteLine(employee);
}
}
}
如果要调用多个方 法,就需要多次显式调用这个委托。但是,委托也可以包含多个方法。这种委托称为多播委托。如果调用多播
委托,就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void;否则,就只能得到委托调用的最 后一个方法的结果。
class MathOperations
{
public static void MultiplyByTwo(double value) =>
Console.WriteLine($"Multiplying by 2: {value} gives {value * 2}");
public static void Square(double value) =>
Console.WriteLine($"Squaring: {value} gives {value * value}");
}
class Program
{
static void Main()
{
Action<double> operations = MathOperations.MultiplyByTwo;
operations += MathOperations.Square;
ProcessAndDisplayNumber(operations, 2.0);
ProcessAndDisplayNumber(operations, 7.94);
ProcessAndDisplayNumber(operations, 1.414);
Console.WriteLine();
}
static void ProcessAndDisplayNumber(Action<double> action, double value)
{
Console.WriteLine();
Console.WriteLine($"ProcessAndDisplayNumber called with value = {value}");
action(value);
}
}
匿名方法是用作委托的参数的一段代码。用匿名方法定义委托的语法与前面的定义并没有区别。但在实例化委托时,就会出现区别。
class Program
{
static void Main()
{
string mid = ", middle part,";
Func<string, string> anonDel = delegate (string param)
{
param += mid;
param += " and this was added to the string.";
return param;
};
Console.WriteLine(anonDel("Start of string"));
}
}
可以看出,该代码块使用方法级的字符串变量mid,该变量是在匿名方法的外部定义的,并将其添加到要传递的参数中。接着代码返回该字符串值。在调用委托时,把一个字符串作为参数传递,将返回的字符串输出 到控制台上。
使用lambda表达式的一个场合是把lambda表达式赋予委托类型:在线实现代码。只要有委托参数类型的地 方,就可以使用lambda表达式。
class Program
{
static void Main()
{
string mid = ",middle part,";
Func<string, string> lambda = param =>
{
param += mid;
param += " and this was added to the string. ";
return param;
};
Console.WriteLine(lambda("Start of string"));
}
}
lambda表达式有几种定义参数的方式。如果只有一个参数,只写出参数名就足够了。下面的lambda表达式 使用了参数So因为委托类型定义了一个string参数,所以s的类型就是string。实现代码调用String.FormatO方法 来返回一个字符串,在调用该委托时,就把该字符串最终写入控制台:
Func<string, string> oneParam = s => $"change uppercase {s .ToUpper () } **;
Console.WriteLine(oneParam("test"));
如果委托使用多个参数,就把这些参数名放在圆括号中。这里参数x和y的类型是double,由Func
Func<double, double, double> twoParams = (x, y) c«=> x * y;
Console.WriteLine(twoParams(3, 2));
为了方便起见,可以在圆括号中给变量名添加参数类型。如果编译器不能匹配重载后的版本,那么使用参
数类型可以帮助找到匹配的委托:
Func<double, double, double> twoParamsWithTypes =
(double x, double y) => x * y;
Console.WriteLine(twoParamsWithTypes(4, 2));
如果lambda表达式只有一条语句,在方法块内就不需要花括号和return语句,因为编译器会添加一条隐式的return 语句:
Func<double, double> square = x => x * x;
添加花括号、return语句和分号是完全合法的,通常这比不添加这些符号更容易阅读:
Func<double,double> square = x =>return x * x;
但是,如果在lambda表达式的实现代码中需要多条语句,就必须添加花括号和return语句:
Func<string, string> lambda = param =>
{
param += mid;
param += " and this was added to the string. ";
return param;
};
通过lambda表达式可以访问lambda表达式块外部的变量,这称为闭包。
int someVal = 5;
Func<int, int> f = x => x + someVal;
someVal = 7;
Console.WriteLine(f(3));
Console.WriteLine();
如果给多个线程使用闭包,就可能遇到并发冲突。最好仅给闭包使用不变的类型。这样可以确保不改变值, 也不需要同步。
事件基于委托,为委托提供了一种发布/订阅机制。在.NET架构内到处都能看到事件。在Windows应用
程序中,Button类提供了 Click事件。这类事件就是委托。触发CUck事件时调用的处理程序方法需要得到定义, 而其参数由委托类型定义。
我们从CarDealer类开始介绍,它基于事件提供一个订阅。在NewCarQ方法中,通过调用 RaiseNewCarInfo 方法触发,这个方法的实现确认委托是否为空,如果不为空,就引发事件。
public class CarInfoEventArgs : EventArgs
{
public CarInfoEventArgs(string car) => Car = car;
public string Car { get; }
}
public class CarDealer
{
public event EventHandler<CarInfoEventArgs> NewCarInfo;
public void NewCar(string car)
{
Console.WriteLine($"CarDealer, new car {car}");
NewCarInfo?.Invoke(this, new CarInfoEventArgs(car));
}
}
CarDealer 类提供了 EventHandler
CarDealer类通过调用委托的Invoke方法触发事件。可以调用给事件订阅的所有处理程序。注意,与之前 的多播委托一样,方法的调用顺序无法保证。为了更多地控制处理程序的调用,可以使用Delegate类的 GetInvocationList()方法,访问委托列表中的每一项,并独立地调用每个方法,如上所示。
Consumer类用作事件侦听器。这个类订阅了 CarDealer类的事件,并定义了 NewCarlsHere方法,该方法满足EventHandler 现在需要连接事件发布程序和订阅器。为此使用CarDealer类的NewCarlnfo事件,通过“+=”创建一个订阅。消费者Valtteri订阅了事件,接着消费者Max也订阅了事件,然后Valtteri通过取消了订阅。 运行应用程序,一辆Williams汽车到达,Valtteri得到了通知。因为之后Max也注册了该订阅,所以Valtteri 和Max都获得了新款Mercedes汽车的通知。接着Valtteri取消了订阅,所以只有Max获得了 Ferrari汽车的 通知:public class Consumer
{
private string _name;
public Consumer(string name) => _name = name;
public void NewCarIsHere(object sender, CarInfoEventArgs e) =>
Console.WriteLine($"{_name}: car {e.Car} is new");
}
class Program
{
static void Main()
{
var dealer = new CarDealer();
var valtteri = new Consumer("Valtteri");
dealer.NewCarInfo += valtteri.NewCarIsHere;
dealer.NewCar("Williams");
var max = new Consumer("Max");
dealer.NewCarInfo += max.NewCarIsHere;
dealer.NewCar("Mercedes");
dealer.NewCarInfo -= valtteri.NewCarIsHere;
dealer.NewCar("Ferrari");
}
}
CarDealer, new car Williams
Valtteri: car Williams is new
CarDealer, new car Mercedes
Valtteri: car Mercedes is new
Max: car Mercedes is new
CarDealer, new car Ferrari
Max: car Ferrari is new