C#8.0本质论第十三章--委托和Lambda表达式
13.1委托概述
C#使用委托提供类似C++里函数指针的功能。委托允许捕捉对方法的引用。
13.1.1背景
13.1.2委托数据类型
13.2声明委托类型
使用delegate关键字声明委托类型
13.2.1常规用途的委托类型:System.Func和System.Action
System.Func系列委托代表有返回值的方法,而System.Action系列代表返回void的方法。
c#
public delegate void Action();
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(
T1 arg1, T2 arg2);
public delegate void Action<in T1, in T2, in T3>(
T1 arg1, T2 arg2, T3 arg3);
public delegate void Action<in T1, in T2, in T3, in T4>(
T1 arg1, T2 arg2, T3 arg3, T4 arg4);
...
public delegate void Action<
in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8,
in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16>(
T1 arg1, T2 arg2, T3 arg3, T4 arg4,
T5 arg5, T6 arg6, T7 arg7, T8 arg8,
T9 arg9, T10 arg10, T11 arg11, T12 arg12,
T13 arg13, T14 arg14, T15 arg15, T16 arg16);
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(
T1 arg1, T2 arg2);
public delegate TResult Func<in T1, in T2, in T3, out TResult>(
T1 arg1, T2 arg2, T3 arg3);
public delegate TResult Func<in T1, in T2, in T3, in T4,
out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
...
public delegate TResult Func<
in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8,
in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16,
out TResult>(
T1 arg1, T2 arg2, T3 arg3, T4 arg4,
T5 arg5, T6 arg6, T7 arg7, T8 arg8,
T9 arg9, T10 arg10, T11 arg11, T12 arg12,
T13 arg13, T14 arg14, T15 arg15, T16 arg16);
public delegate bool Predicate<in T>( T obj)
Func的最后一个类型参数是TResult,即返回值的类型。
清单最后一个委托是Predicate< in T >。若用一个Lambda返回bool,则该Lambda称为谓词(predicate)通常用谓词筛选或标识集合中的数据项。
13.2.2实例化委托
委托是引用类型,但不需要用new实例化。从C#2.0开始,从方法组(为方法命名的表达式)向委托类型的转换会自动创建新的委托对象。
委托实际是特殊的类。虽然C#标准没有明确说明类的层次结构,但委托必须直接或间接派生自System.Delegate。
第一个属性是System.Reflection.MethodInfo类型,描述方法签名,包括名称、参数和返回类型。除了MethodInfo,委托还需要一个对象实例来包含要调用的方法。这正是第二个属性Target的作用。在静态方法的情况下,Target为null。
所有委托都不可空(immutable)。换言之,委托创建好后无法更改。
13.3Lambda表达式
C#2.0引入了匿名方法 ,C#3.0引入了Lambda表达式 。这两种语法统称为匿名函数。这两种都合法,但应该优先使用Lambda表达式。
Lambda表达式的目的是在需要基于很简单的方法生成委托时,避免声明全新成员的麻烦。
Lambda表达式分成两种:语句Lambda 和表达式Lambda
13.3.1语句Lambda
语句Lambda由形参列表、Lambda操作符=>和代码块构成。
c#
BubbleSort(items, (int first, int second) =>
{
return first < second;
}
);
假如只有一个参数,且类型可以推断,Lambda表达式就可拿掉围绕参数列表的圆括号。但假如无参,或参数不止一个,或者包含显示指定了类型的单个参数,Lambda表达式就必须将参数列表放到圆括号中。
c#
// ...
IEnumerable<Process> processes = Process.GetProcesses().Where(
process => { return process.WorkingSet64 > 1000000000; });
// ...
c#
Func<string> getUserInput =
() =>
{
string? input;
do
{
input = Console.ReadLine();
}
while(!string.IsNullOrWhiteSpace(input));
return input!;
};
//...
13.3.2表达式Lambda
只需要包含要返回的表达式,完全没有语句块。
c#
//...
DelegateSample.BubbleSort(items, (first, second) => first < second);
//...
13.4匿名方法
匿名方法必须显式指定每个参数的类型,而且必须有代码块。在参数列表前添加delegate关键字
c#
//...
DelegateSample.BubbleSort(items,
delegate(int first, int second)
{
return first < second;
}
);
//...
13.5委托没有结构相等性
.NET委托类型不具有结构相等性(structural equality),也就是说,不能将一个委托类型的对象引用转换成一个不相关的委托类型,即使两者的形参和返回类型完全一致。唯一的办法就算创建新委托并让它引用旧委托的Invoke方法。
13.6外部变量
在Lambda表达式外部声明的局部变量称为该Lambda的外部变量。如果Lambda表达式主体使用一个外部变量,那么就说该变量被该Lambda表达式捕获。被捕捉的变量的生存期被延长了。
外部变量的CIL实现
c#
public class Program
{
// ...
private sealed class __LocalsDisplayClass_00000001
{
public int comparisonCount;
public bool __AnonymousMethod_00000000(
int first, int second)
{
comparisonCount++;
return first < second;
}
}
public static void Main()
{
__LocalsDisplayClass_00000001 locals = new();
locals.comparisonCount = 0;
int[] items = new int[5];
for (int i = 0; i < items.Length; i++)
{
Console.Write("Enter an integer: ");
string? text = Console.ReadLine();
if (!int.TryParse(text, out items[i]))
{
Console.WriteLine($"'{text}' is not a valid integer.");
return;
}
}
DelegateSample.BubbleSort
(items, locals.__AnonymousMethod_00000000);
for (int i = 0; i < items.Length; i++)
{
Console.WriteLine(items[i]);
}
Console.WriteLine("Items were compared {0} times.",
locals.comparisonCount);
}
}
注意,被捕捉的局部变量永远不会被"传递"或"拷贝"到别的地方。相反,作为实例字段实现,从而延长了生存期。
生成的__LocalsDisplayClass类称为闭包(closure),它是一个数据结构(一个C#类),其中包含一个表达式以及对表达式进行求值所需的变量(C#中的公共字段)。
在C#5.0中捕捉循环变量:
c#
public static void Main()
{
var items = new string[] { "Moe", "Larry", "Curly" };
var actions = new List<Action>();
foreach(string item in items)
{
actions.Add(() => { Console.WriteLine(item); });
}
foreach(Action action in actions)
{
action();
}
}
在C#4.0中输出的是
Curly
Curly
Curly
Lambda表达式捕捉变量并总是使用其最新的值--而不是捕捉并保留变量在委托创建时的值。这正是你希望的行为。
捕捉循环变量时,每个委托都捕捉同一个循环变量。循环变量变化时,捕捉它的每个委托都看到了变化。所以无法指责C#4.0的行为。
C#5.0对此进行了更改,认为每一次循环迭代,foreach循环变量都应该是"新"变量。所以,每次创建委托,捕捉的都是不同的变量,不再共享同一个变量。但注意,这个更改不适用于for循环。
在C#5.0之前的版本中应使用下面的模式:
c#
public static void Main()
{
var items = new string[] { "Moe", "Larry", "Curly" };
var actions = new List<Action>();
foreach(string item in items)
{
string _item = item;
actions.Add( () => { Console.WriteLine(_item); } );
}
foreach(Action action in actions)
{
action();
}
}
13.7表达式树
表达式Lambda还能转换成表达式树,表达式树也是对象,允许传递编译器对Lambda表达式的分析。
13.7.1Lambda表达式作为数据使用
c#
persons.Where( person => person.Name.ToUpper() == "INIGO MONTOYA");
假定persons是Person的一个数组,和Lambda表达式实参对应的Where方法形参具有委托类型Func< Person,bool >。编译器生成方法来包含Lambda表达式主体代码,再创建委托实例来代表所生成的方法,并将委托传给Where方法。Where方法返回一个查询对象,一旦执行查询,就将委托应用于数组的每个成员来判断查询结果。
现在假定persons是代表远程数据库表的对象,表中有数百万人的数据,客户端如何请求查询结果?
一个技术是将几百万数据从服务器传输到客户端,每一行都创建Person对象,根据Lambda创建委托,再针对每个Person执行委托。但代价过于高昂。
第二个技术则要好很多,它是将Lambda的含义发送给服务器。服务器只将符合条件的少数几行传输到客户端,而不是创建几百万个对象。但怎样将Lambda的含义发送给服务器呢?
这正是在语言中添加表达式树的原因。转换成表达式树的Lambda表达式对象代表的是对Lambda表达式进行描述的数据,而不是编译好的、用于实现匿名函数的代码。
c#
EXPRESSION TREE:
persons.Where(person => person.Name.ToUpper() == "INIGO MONTOYA");
SQL WHERE CLAUSE:
select * from Person where upper(Name) = 'INIGO MONTOYA';