十二、委托、事件和 Lambda 表达式
到本文的这一点为止,你开发的大多数应用都将不同的代码作为顶级语句添加到了Program.cs
中,这些语句以某种方式将请求发送到给定对象的。然而,许多应用要求一个对象能够使用回调机制将传递回创建它的实体。虽然回调机制可以在任何应用中使用,但它们对于图形用户界面尤其重要,因为控件(如按钮)需要在正确的情况下(单击按钮时、鼠标进入按钮表面时等)调用外部方法。).
在下面。NET 核心平台中,委托类型是在应用中定义和响应回调的首选方式。本质上。NET Core 委托类型是一个类型安全的对象,它“指向”一个方法或一组以后可以调用的方法。然而,与传统的 C++函数指针不同,委托是具有多播内置支持的类。
Note
在的早期版本中。NET 中,用BeginInvoke()
/ EndInvoke()
委托公开的异步方法调用。虽然这些仍由编译器生成,但在下不受支持。NET 核心。这是因为代理使用的IAsyncResult()
/ BeginInvoke()
模式已经被基于任务的异步模式所取代。有关异步执行的更多信息,请参见第十五章。
在本章中,你将学习如何创建和操作委托类型,然后你将研究 C# event
关键字,它简化了使用委托类型的过程。在此过程中,您还将研究 C# 的几个以委托为中心和以事件为中心的语言特性,包括匿名方法和方法组转换。
我通过检查λ表达式来结束这一章。使用 C# lambda 运算符(=>
),您可以在任何需要强类型委托的地方指定代码语句块(以及传递给这些代码语句的参数)。正如您将看到的,lambda 表达式只不过是一个伪装的匿名方法,并提供了一种简化的委托处理方法。此外,相同的操作(截至。NET Framework 4.6 及更高版本)可用于使用简洁的语法实现单语句方法或属性。
了解委托类型
在正式定义代表之前,让我们先了解一下情况。历史上,Windows API 经常使用 C 风格的函数指针来创建被称为回调函数的实体,或者简称为回调。使用回调,程序员能够配置一个函数来报告(回调)应用中的另一个函数。通过这种方法,Windows 开发人员能够处理按钮点击、鼠标移动、菜单选择以及内存中两个实体之间的一般双向通信。
在。NET 和。NET 核心框架中,回调是使用委托以类型安全和面向对象的方式完成的。委托是一个类型安全的对象,它指向应用中的另一个方法(或者可能是一列方法),以后可以调用该方法。具体来说,代理维护三条重要的信息。
-
它调用的方法的地址
-
该方法的参数(如果有)
-
该方法的返回类型(如果有)
Note
。NET 核心委托可以指向静态方法或实例方法。
在委托对象被创建并被赋予必要的信息后,它可以在运行时动态地调用它所指向的方法。
在 C# 中定义委托类型
当你想在 C# 中创建一个委托类型时,你可以使用delegate
关键字。您的委托类型的名称可以是您想要的任何名称。但是,您必须定义委托以匹配它将指向的方法的签名。例如,下面的委托类型(名为BinaryOp
)可以指向任何返回一个整数并接受两个整数作为输入参数的方法(在本章的稍后部分,您将自己构建并使用这个委托,所以暂时不要着急):
// This delegate can point to any method,
// taking two integers and returning an integer.
public delegate int BinaryOp(int x, int y);
当 C# 编译器处理委托类型时,它会自动生成一个从System.MulticastDelegate
派生的密封类。这个类(与它的基类System.Delegate
一起)为委托提供了必要的基础结构,以保存稍后要调用的方法列表。例如,如果您使用ildasm.exe
来检查BinaryOp
委托,您会发现如下所示的细节(如果您想自己检查,您将马上构建这个完整的示例):
// -------------------------------------------------------
// TypDefName: SimpleDelegate.BinaryOp
// Extends : System.MulticastDelegate
// Method #1
// -------------------------------------------------------
// MethodName: .ctor
// ReturnType: Void
// 2 Arguments
// Argument #1: Object
// Argument #2: I
// Method #2
// -------------------------------------------------------
// MethodName: Invoke
// ReturnType: I4
// 2 Arguments
// Argument #1: I4
// Argument #2: I4
// 2 Parameters
// (1) ParamToken : Name : x flags: [none]
// (2) ParamToken : Name : y flags: [none] //
// Method #3
// -------------------------------------------------------
// MethodName: BeginInvoke
// ReturnType: Class System.IAsyncResult
// 4 Arguments
// Argument #1: I4
// Argument #2: I4
// Argument #3: Class System.AsyncCallback
// Argument #4: Object
// 4 Parameters
// (1) ParamToken : Name : x flags: [none]
// (2) ParamToken : Name : y flags: [none]
// (3) ParamToken : Name : callback flags: [none]
// (4) ParamToken : Name : object flags: [none]
//
// Method #4
// -------------------------------------------------------
// MethodName: EndInvoke
// ReturnType: I4 (int32)
// 1 Arguments
// Argument #1: Class System.IAsyncResult
// 1 Parameters
// (1) ParamToken : Name : result flags: [none]
如您所见,编译器生成的BinaryOp
类定义了三个公共方法。Invoke()
是中的关键方法。NET Core,因为它用于以一种同步的方式调用由委托对象维护的每个方法,这意味着调用者必须等待调用完成后才能继续它的方式。奇怪的是,同步Invoke()
方法可能不需要从 C# 代码中显式调用。正如您马上会看到的,当您使用适当的 C# 语法时,Invoke()
会在幕后被调用。
Note
虽然生成了BeginInvoke()
和EndInvoke()
,但是在下运行代码时不支持它们。NET 核心。这可能会令人沮丧,因为如果使用它们,您将不会收到编译器错误,而是运行时错误。
现在,编译器到底是如何知道如何定义Invoke()
方法的呢?为了理解这个过程,下面是编译器生成的BinaryOp
类类型的关键(粗斜体标记了由定义的委托类型指定的项目):
sealed class BinaryOp : System.MulticastDelegate
{
public int Invoke(int x, int y);
...
}
首先,注意为Invoke()
方法定义的参数和返回类型与BinaryOp
委托的定义完全匹配。
让我们看另一个例子。假设您已经定义了一个委托类型,它可以指向任何返回一个string
并接收三个System.Boolean
输入参数的方法。
public delegate string MyDelegate (bool a, bool b, bool c);
这一次,编译器生成的类分解如下:
sealed class MyDelegate : System.MulticastDelegate
{
public string Invoke(bool a, bool b, bool c);
...
}
委托还可以“指向”包含任意数量的out
或ref
参数(以及标有params
关键字的数组参数)的方法。例如,假设以下委托类型:
public delegate string MyOtherDelegate(
out bool a, ref bool b, int c);
Invoke()
方法的签名看起来就像你所期望的那样。
总的来说,C# 委托类型定义会产生一个密封类,其中包含一个编译器生成的方法,该方法的参数和返回类型基于委托的声明。以下伪代码近似于基本模式:
// This is only pseudo-code!
public sealed class DelegateName : System.MulticastDelegate
{
public delegateReturnValue Invoke(allDelegateInputRefAndOutParams);
}
系统。多播代理和系统。委托基类
因此,当您使用 C# delegate
关键字构建类型时,您是在间接声明一个从System.MulticastDelegate
派生的类类型。该类为后代提供对列表的访问,该列表包含由委托对象维护的方法的地址,以及与调用列表交互的几个附加方法(和几个重载运算符)。以下是System.MulticastDelegate
的一些精选成员:
public abstract class MulticastDelegate : Delegate
{
// Returns the list of methods "pointed to."
public sealed override Delegate[] GetInvocationList();
// Overloaded operators.
public static bool operator ==
(MulticastDelegate d1, MulticastDelegate d2);
public static bool operator !=
(MulticastDelegate d1, MulticastDelegate d2);
// Used internally to manage the list of methods maintained by the delegate.
private IntPtr _invocationCount;
private object _invocationList;
}
System.MulticastDelegate
从其父类System.Delegate
获得附加功能。下面是类定义的部分快照:
public abstract class Delegate : ICloneable, ISerializable
{
// Methods to interact with the list of functions.
public static Delegate Combine(params Delegate[] delegates);
public static Delegate Combine(Delegate a, Delegate b);
public static Delegate Remove(
Delegate source, Delegate value);
public static Delegate RemoveAll(
Delegate source, Delegate value);
// Overloaded operators.
public static bool operator ==(Delegate d1, Delegate d2);
public static bool operator !=(Delegate d1, Delegate d2);
// Properties that expose the delegate target.
public MethodInfo Method { get; }
public object Target { get; }
}
现在,要明白你永远不能在你的代码中直接从这些基类派生(这样做是一个编译器错误)。然而,当您使用delegate
关键字时,您已经间接地创建了一个“is-a”MulticastDelegate
类。表 12-1 记录了所有委托类型共有的核心成员。
表 12-1。
选择System.MulticastDelegate/System.Delegate的成员
|成员
|
生命的意义
|
| — | — |
| Method
| 该属性返回一个代表由委托维护的静态方法的细节的System.Reflection.MethodInfo
对象。 |
| Target
| 如果要调用的方法是在对象级定义的(而不是静态方法),Target
返回一个对象,该对象表示由委托维护的方法。如果从Target
返回的值等于null
,那么要调用的方法是一个静态成员。 |
| Combine()
| 此静态方法将方法添加到由委托维护的列表中。在 C# 中,您使用重载的+=
操作符作为一种简写符号来触发这个方法。 |
| GetInvocationList()
| 这个方法返回一个由System.Delegate
对象组成的数组,每个对象代表一个可能被调用的方法。 |
| Remove()
/``RemoveAll()
| 这些静态方法从委托的调用列表中移除一个方法(或所有方法)。在 C# 中,可以使用重载的-=
运算符间接调用Remove()
方法。 |
最简单的委托示例
当然,第一次遇到委托时,可能会引起一些混乱。因此,开始吧,让我们看一个简单的控制台应用(名为 SimpleDelegate ),它使用了您之前见过的BinaryOp
委托类型。下面是完整的代码,并附有分析:
//SimpleMath.cs
namespace SimpleDelegate
{
// This class contains methods BinaryOp will
// point to.
public class SimpleMath
{
public static int Add(int x, int y) => x + y;
public static int Subtract(int x, int y) => x - y;
}
}
//Program.cs
using System;
using SimpleDelegate;
Console.WriteLine("***** Simple Delegate Example *****\n");
// Create a BinaryOp delegate object that
// "points to" SimpleMath.Add().
BinaryOp b = new BinaryOp(SimpleMath.Add);
// Invoke Add() method indirectly using delegate object.
Console.WriteLine("10 + 10 is {0}", b(10, 10));
Console.ReadLine();
//Additional type definitions must be placed at the end of the
// top-level statements
// This delegate can point to any method,
// taking two integers and returning an integer.
public delegate int BinaryOp(int x, int y);
Note
回想一下第三章中的内容,额外的类型声明(在这个例子中是BinaryOp
委托)必须跟在所有的顶级语句之后。
再次注意BinaryOp
委托类型声明的格式;它指定BinaryOp
委托对象可以指向任何一个接受两个整数并返回一个整数的方法(所指向的方法的实际名称是不相关的)。这里,您已经创建了一个名为SimpleMath
的类,它定义了两个静态方法,这两个方法与BinaryOp
委托定义的模式相匹配。
当您想要将目标方法分配给给定的委托对象时,只需将方法的名称传递给委托的构造函数。
// Create a BinaryOp delegate object that
// "points to" SimpleMath.Add().
BinaryOp b = new BinaryOp(SimpleMath.Add);
此时,您可以使用类似于直接函数调用的语法来调用所指向的成员。
// Invoke() is really called here!
Console.WriteLine("10 + 10 is {0}", b(10, 10));
在幕后,运行时在您的MulticastDelegate
派生类上调用编译器生成的Invoke()
方法。如果您在ildasm.exe
中打开您的程序集,并在Main()
方法中检查 CIL 代码,您可以自己验证这一点。
.method private hidebysig static void Main(string[] args) cil managed
{
...
callvirt instance int32 BinaryOp::Invoke(int32, int32)
}
C# 不要求你在代码库中显式调用Invoke()
。因为BinaryOp
可以指向带两个参数的方法,下面的代码语句也是允许的:
Console.WriteLine("10 + 10 is {0}", b.Invoke(10, 10));
回想一下。NET 核心委托是类型安全的。因此,如果您试图创建一个指向与模式不匹配的方法的委托对象,就会收到一个编译时错误。举例来说,假设SimpleMath
类现在定义了一个名为SquareNumber()
的附加方法,它接受一个整数作为输入。
public class SimpleMath
{
public static int SquareNumber(int a) => a * a;
}
鉴于BinaryOp
委托只能将指向接受两个整数并返回一个整数的方法,下面的代码是非法的,不会被编译:
// Compiler error! Method does not match delegate pattern!
BinaryOp b2 = new BinaryOp(SimpleMath.SquareNumber);
调查委托对象
让我们通过在Program
类中创建一个静态方法(名为DisplayDelegateInfo()
)来增加当前示例的趣味。该方法将打印出由委托对象维护的方法的名称,以及定义该方法的类的名称。为此,您将迭代由GetInvocationList()
返回的System.Delegate
数组,调用每个对象的Target
和Method
属性。
static void DisplayDelegateInfo(Delegate delObj)
{
// Print the names of each member in the
// delegate's invocation list.
foreach (Delegate d in delObj.GetInvocationList())
{
Console.WriteLine("Method Name: {0}", d.Method);
Console.WriteLine("Type Name: {0}", d.Target);
}
}
假设您已经更新了您的Main()
方法来调用这个新的帮助器方法,如下所示:
BinaryOp b = new BinaryOp(SimpleMath.Add);
DisplayDelegateInfo(b);
您会发现如下所示的输出:
***** Simple Delegate Example *****
Method Name: Int32 Add(Int32, Int32)
Type Name:
10 + 10 is 20
注意,当调用Target
属性时,目标类(SimpleMath
)的名称当前显示为而不是。原因是您的BinaryOp
委托指向一个静态方法,因此没有对象可以引用!然而,如果您将Add()
和Subtract()
方法更新为非静态的(只需删除static
关键字),您可以创建一个SimpleMath
类的实例,并使用对象引用指定要调用的方法。
using System;
using SimpleDelegate;
Console.WriteLine("***** Simple Delegate Example *****\n");
// Delegates can also point to instance methods as well.
SimpleMath m = new SimpleMath();
BinaryOp b = new BinaryOp(m.Add);
// Show information about this object.
DisplayDelegateInfo(b);
Console.WriteLine("10 + 10 is {0}", b(10, 10));
Console.ReadLine();
在这种情况下,您会发现如下所示的输出:
***** Simple Delegate Example *****
Method Name: Int32 Add(Int32, Int32)
Type Name: SimpleDelegate.SimpleMath
10 + 10 is 20
使用委托发送对象状态通知
显然,前面的 SimpleDelegate 示例本质上纯粹是说明性的,因为没有令人信服的理由来定义一个简单地将两个数相加的委托。为了更真实地使用委托类型,让我们使用委托来定义一个Car
类,它可以通知外部实体它当前的引擎状态。为此,您将采取以下步骤:
-
定义将用于向呼叫者发送通知的新委托类型。
-
在
Car
类中声明这个委托的成员变量。 -
在
Car
上创建一个助手函数,允许调用者指定要回调的方法。 -
实现
Accelerate()
方法以在正确的情况下调用委托的调用列表。
首先,创建一个名为 CarDelegate 的新控制台应用项目。现在,定义一个新的Car
类,最初如下所示:
using System;
using System.Linq;
namespace CarDelegate
{
public class Car
{
// Internal state data.
public int CurrentSpeed { get; set; }
public int MaxSpeed { get; set; } = 100;
public string PetName { get; set; }
// Is the car alive or dead?
private bool _carIsDead;
// Class constructors.
public Car() {}
public Car(string name, int maxSp, int currSp)
{
CurrentSpeed = currSp;
MaxSpeed = maxSp;
PetName = name;
}
}
}
现在,考虑以下更新,这些更新解决了前三点:
public class Car
{
...
// 1) Define a delegate type.
public delegate void CarEngineHandler(string msgForCaller);
// 2) Define a member variable of this delegate.
private CarEngineHandler _listOfHandlers;
// 3) Add registration function for the caller.
public void RegisterWithCarEngine(CarEngineHandler methodToCall)
{
_listOfHandlers = methodToCall;
}
}
请注意,在这个例子中,您直接在Car
类的范围内定义了委托类型,这当然不是必需的,但确实有助于强化委托自然地与这个类一起工作的思想。委托类型CarEngineHandler
可以指向任何将单个string
作为输入并将void
作为返回值的方法。
接下来,请注意,您声明了一个委托类型的私有成员变量(名为_listOfHandlers
)和一个助手函数(名为RegisterWithCarEngine()
),该函数允许调用者将一个方法分配给委托的调用列表。
Note
严格地说,您可以将您的委托成员变量定义为 public,从而避免创建额外的注册方法。但是,通过将委托成员变量定义为 private,您可以实施封装服务并提供更类型安全的解决方案。在本章的后面,当你查看 C# event
关键字时,你将再次讨论公共委托成员变量的风险。
此时,您需要创建Accelerate()
方法。回想一下,这里的要点是允许一个Car
对象向任何订阅的侦听器发送与引擎相关的消息。以下是最新消息:
// 4) Implement the Accelerate() method to invoke the delegate's
// invocation list under the correct circumstances.
public void Accelerate(int delta)
{
// If this car is "dead," send dead message.
if (_carIsDead)
{
_listOfHandlers?.Invoke("Sorry, this car is dead...");
}
else
{
CurrentSpeed += delta;
// Is this car "almost dead"?
if (10 == (MaxSpeed - CurrentSpeed))
{
_listOfHandlers?.Invoke("Careful buddy! Gonna blow!");
}
if (CurrentSpeed >= MaxSpeed)
{
_carIsDead = true;
}
else
{
Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed);
}
}
}
请注意,在尝试调用由listOfHandlers
成员变量维护的方法时,您使用了空传播语法。原因是调用者的工作是通过调用RegisterWithCarEngine()
helper 方法来分配这些对象。如果调用者没有调用这个方法,而你试图调用委托的调用列表,你将在运行时触发一个NullReferenceException
。现在您已经有了委托基础设施,观察对Program
类的更新,如下所示:
using System;
using CarDelegate;
Console.WriteLine("** Delegates as event enablers **\n");
// First, make a Car object.
Car c1 = new Car("SlugBug", 100, 10);
// Now, tell the car which method to call
// when it wants to send us messages.
c1.RegisterWithCarEngine(
new Car.CarEngineHandler(OnCarEngineEvent));
// Speed up (this will trigger the events).
Console.WriteLine("***** Speeding up *****");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();
// This is the target for incoming events.
static void OnCarEngineEvent(string msg)
{
Console.WriteLine("\n*** Message From Car Object ***");
Console.WriteLine("=> {0}", msg);
Console.WriteLine("********************\n");
}
代码从简单地创建一个新的Car
对象开始。既然您对引擎事件感兴趣,那么下一步就是调用您的定制注册函数RegisterWithCarEngine()
。回想一下,这个方法期望被传递一个嵌套的CarEngineHandler
委托的实例,和任何委托一样,您指定一个“指向的方法”作为构造函数参数。本例中的技巧是,所讨论的方法位于Program
类中!再次注意,OnCarEngineEvent()
方法与相关委托完全匹配,因为它接受一个string
作为输入并返回void
。考虑当前示例的输出:
***** Delegates as event enablers *****
***** Speeding up *****
CurrentSpeed = 30
CurrentSpeed = 50
CurrentSpeed = 70
***** Message From Car Object *****
=> Careful buddy! Gonna blow!
***********************************
CurrentSpeed = 90
***** Message From Car Object *****
=> Sorry, this car is dead...
***********************************
启用多播
回想一下.NETCore 代表具有内置的组播能力。换句话说,委托对象可以维护要调用的方法列表,而不仅仅是单个方法。当你想给一个委托对象添加多个方法时,你只需使用重载的+=
操作符,而不是直接赋值。要在Car
类上启用多播,您可以更新RegisterWithCarEngine()
方法,如下所示:
public class Car
{
// Now with multicasting support!
// Note we are now using the += operator, not
// the assignment operator (=).
public void RegisterWithCarEngine(
CarEngineHandler methodToCall)
{
_listOfHandlers += methodToCall;
}
...
}
当您在委托对象上使用+=
操作符时,编译器将其解析为对静态Delegate.Combine()
方法的调用。事实上,你可以直接给Delegate.Combine()
打电话;然而,+=
操作符提供了一个更简单的选择。不需要修改您当前的RegisterWithCarEngine()
方法,但是这里有一个使用Delegate.Combine()
而不是+=
操作符的例子:
public void RegisterWithCarEngine( CarEngineHandler methodToCall )
{
if (_listOfHandlers == null)
{
_listOfHandlers = methodToCall;
}
else
{
_listOfHandlers =
Delegate.Combine(_listOfHandlers, methodToCall)
as CarEngineHandler;
}
}
无论如何,调用者现在可以为同一个回调通知注册多个目标。这里,第二个处理程序以大写形式打印传入的消息,只是为了显示:
Console.WriteLine("***** Delegates as event enablers *****\n");
// First, make a Car object.
Car c1 = new Car("SlugBug", 100, 10);
// Register multiple targets for the notifications.
c1.RegisterWithCarEngine(
new Car.CarEngineHandler(OnCarEngineEvent));
c1.RegisterWithCarEngine(
new Car.CarEngineHandler(OnCarEngineEvent2));
// Speed up (this will trigger the events).
Console.WriteLine("***** Speeding up *****");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();
// We now have TWO methods that will be called by the Car
// when sending notifications.
static void OnCarEngineEvent(string msg)
{
Console.WriteLine("\n*** Message From Car Object ***");
Console.WriteLine("=> {0}", msg);
Console.WriteLine("*********************************\n");
}
static void OnCarEngineEvent2(string msg)
{
Console.WriteLine("=> {0}", msg.ToUpper());
}
从委托的调用列表中删除目标
Delegate
类还定义了一个静态的Remove()
方法,允许调用者从委托对象的调用列表中动态删除一个方法。这使得允许调用者在运行时“取消订阅”给定的通知变得简单。虽然您可以在代码中直接调用Delegate.Remove()
,但是 C# 开发人员可以使用-=
操作符作为一种方便的简写符号。让我们给Car
类添加一个新方法,它允许调用者从调用列表中删除一个方法。
public class Car
{
...
public void UnRegisterWithCarEngine(CarEngineHandler methodToCall)
{
_listOfHandlers -= methodToCall;
}
}
使用当前对Car
类的更新,您可以通过更新调用代码来停止在第二个处理程序上接收引擎通知,如下所示:
Console.WriteLine("***** Delegates as event enablers *****\n");
// First, make a Car object.
Car c1 = new Car("SlugBug", 100, 10);
c1.RegisterWithCarEngine(
new Car.CarEngineHandler(OnCarEngineEvent));
// This time, hold onto the delegate object,
// so we can unregister later.
Car.CarEngineHandler handler2 =
new Car.CarEngineHandler(OnCarEngineEvent2);
c1.RegisterWithCarEngine(handler2);
// Speed up (this will trigger the events).
Console.WriteLine("***** Speeding up *****");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
// Unregister from the second handler.
c1.UnRegisterWithCarEngine(handler2);
// We won't see the "uppercase" message anymore!
Console.WriteLine("***** Speeding up *****");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();
这段代码中的一个不同之处是,这次您创建了一个Car.CarEngineHandler
对象,并将其存储在一个局部变量中,这样您就可以在以后使用该对象来注销通知。因此,第二次加速Car
对象时,您将不再看到输入消息数据的大写版本,因为您已经从委托的调用列表中删除了这个目标。
方法组转换语法
在前面的 CarDelegate 示例中,您显式创建了Car.CarEngineHandler
delegate 对象的实例,以便向引擎通知注册和注销。
Console.WriteLine("***** Delegates as event enablers *****\n");
Car c1 = new Car("SlugBug", 100, 10);
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
Car.CarEngineHandler handler2 =
new Car.CarEngineHandler(OnCarEngineEvent2);
c1.RegisterWithCarEngine(handler2);
...
可以肯定的是,如果您需要调用MulticastDelegate
或Delegate
的任何继承成员,手动创建一个委托变量是最简单的方法。然而,在大多数情况下,您并不真正需要抓住委托对象不放。相反,您通常只需要使用委托对象将方法名作为构造函数参数传入。
作为一种简化,C# 提供了一种称为方法组转换的快捷方式。当调用以委托作为参数的方法时,此功能允许您提供直接的方法名,而不是委托对象。
Note
正如你将在本章后面看到的,你也可以使用方法组转换语法来简化你注册 C# 事件的方式。
举例来说,考虑下面对Program
类的更新,该类使用方法组转换来注册和注销引擎通知:
...
Console.WriteLine("***** Method Group Conversion *****\n");
Car c2 = new Car();
// Register the simple method name.
c2.RegisterWithCarEngine(OnCarEngineEvent);
Console.WriteLine("***** Speeding up *****");
for (int i = 0; i < 6; i++)
{
c2.Accelerate(20);
}
// Unregister the simple method name.
c2.UnRegisterWithCarEngine(OnCarEngineEvent);
// No more notifications!
for (int i = 0; i < 6; i++)
{
c2.Accelerate(20);
}
Console.ReadLine();
请注意,您不是直接分配相关的委托对象,而是简单地指定一个与委托的预期签名相匹配的方法(在本例中,该方法返回void
并接受一个string
)。要明白 C# 编译器仍然在确保类型安全。因此,如果OnCarEngineEvent()
方法没有接受string
并返回void
,就会出现编译器错误。
了解泛型委托
在第十章中,我提到 C# 允许你定义通用的委托类型。例如,假设您想要定义一个委托类型,它可以调用任何返回void
并接收单个参数的方法。如果所讨论的参数可能不同,您可以使用类型参数对此进行建模。为了说明这一点,请考虑名为 GenericDelegate 的新控制台应用项目中的以下代码:
Console.WriteLine("***** Generic Delegates *****\n");
// Register targets.
MyGenericDelegate<string> strTarget =
new MyGenericDelegate<string>(StringTarget);
strTarget("Some string data");
//Using the method group conversion syntax
MyGenericDelegate<int> intTarget = IntTarget;
intTarget(9);
Console.ReadLine();
static void StringTarget(string arg)
{
Console.WriteLine("arg in uppercase is: {0}", arg.ToUpper());
}
static void IntTarget(int arg)
{
Console.WriteLine("++arg is: {0}", ++arg);
}
// This generic delegate can represent any method
// returning void and taking a single parameter of type T.
public delegate void MyGenericDelegate<T>(T arg);
注意,MyGenericDelegate<T>
定义了一个类型参数,它表示传递给委托目标的参数。创建此类型的实例时,需要指定类型参数的值,以及委托将调用的方法的名称。因此,如果您指定了一个字符串类型,您将向目标方法发送一个字符串值。
// Create an instance of MyGenericDelegate<T>
// with string as the type parameter.
MyGenericDelegate<string> strTarget = StringTarget;
strTarget("Some string data");
给定strTarget
对象的格式,StringTarget()
方法现在必须将单个字符串作为参数。
static void StringTarget(string arg)
{
Console.WriteLine(
"arg in uppercase is: {0}", arg.ToUpper());
}
通用动作<>和功能<>委托
在本章的过程中,你已经看到了当你想在你的应用中使用委托来启用回调时,你通常遵循如下所示的步骤:
-
定义与所指向方法的格式相匹配的自定义委托。
-
创建自定义委托的实例,将方法名作为构造函数参数传入。
-
通过调用委托对象上的
Invoke()
来间接调用该方法。
当您采用这种方法时,您通常会得到几个自定义委托,这些委托可能永远不会在当前任务之外使用(例如,MyGenericDelegate<T>
、CarEngineHandler
等)。).虽然您确实需要为您的项目定制一个唯一命名的委托类型,但是其他时候委托类型的确切名称是不相关的。在许多情况下,您只是希望“某个委托”接受一组参数,并可能有一个不同于void
的返回值。在这些情况下,您可以使用框架内置的Action<>
和Func<>
委托类型。为了说明它们的用途,创建一个名为 ActionAndFuncDelegates 的新控制台应用项目。
泛型Action<>
委托是在System
名称空间中定义的,您可以使用这个泛型委托来“指向”一个最多占用 16 个参数的方法(这应该足够了!)并返回void
。现在回想一下,因为Action<>
是一个泛型委托,所以您还需要指定每个参数的底层类型。
更新您的Program
类来定义一个新的静态方法,它接受三个(或更多)唯一的参数。这里有一个例子:
// This is a target for the Action<> delegate.
static void DisplayMessage(string msg, ConsoleColor txtColor, int printCount)
{
// Set color of console text.
ConsoleColor previous = Console.ForegroundColor;
Console.ForegroundColor = txtColor;
for (int i = 0; i < printCount; i++)
{
Console.WriteLine(msg);
}
// Restore color.
Console.ForegroundColor = previous;
}
现在,您可以使用现成的Action<>
委托,而不是手动构建一个自定义委托来将程序流传递给DisplayMessage()
方法,如下所示:
Console.WriteLine("***** Fun with Action and Func *****");
// Use the Action<> delegate to point to DisplayMessage.
Action<string, ConsoleColor, int> actionTarget =
DisplayMessage;
actionTarget("Action Message!", ConsoleColor.Yellow, 5);
Console.ReadLine();
如您所见,使用Action<>
委托可以省去定义定制委托类型的麻烦。然而,回想一下,Action<>
委托类型只能指向采用void
返回值的方法。如果您想指向一个确实有返回值的方法(并且不想麻烦自己编写自定义委托),您可以使用Func<>
。
通用的Func<>
委托可以指向(像Action<>
)最多接受 16 个参数和一个自定义返回值的方法。为了举例说明,将下面的新方法添加到Program
类中:
// Target for the Func<> delegate.
static int Add(int x, int y)
{
return x + y;
}
在本章的前面,我让你构建一个定制的BinaryOp
委托来“指向”加减法。然而,您可以使用一个总共有三个类型参数的版本的Func<>
来简化您的工作。要知道Func<>
的 final 类型参数是总是方法的返回值。为了巩固这一点,假设Program
类还定义了以下方法:
static string SumToString(int x, int y)
{
return (x + y).ToString();
}
现在,调用代码可以调用这些方法中的每一个,如下所示:
Func<int, int, int> funcTarget = Add;
int result = funcTarget.Invoke(40, 40);
Console.WriteLine("40 + 40 = {0}", result);
Func<int, int, string> funcTarget2 = SumToString;
string sum = funcTarget2(90, 300);
Console.WriteLine(sum);
在任何情况下,考虑到Action<>
和Func<>
可以让您省去手动定义自定义委托的步骤,您可能想知道是否应该一直使用它们。与编程的许多方面一样,答案是“视情况而定”在许多情况下,Action<>
和Func<>
将是首选的行动方案(没有双关语)。但是,如果您需要一个具有自定义名称的委托,并且您认为它有助于更好地捕获您的问题域,那么构建自定义委托就像一条代码语句一样简单。在阅读本文的剩余部分时,您将会看到这两种方法。
Note
许多重要的。NET 核心 API 大量使用了Action<>
和Func<>
委托,包括并行编程框架和 LINQ(等等)。
这就结束了我们对委托类型的初步了解。接下来,让我们继续讨论 C# event
关键字的相关主题。
了解 C# 事件
委托是有趣的构造,因为它们使内存中的对象能够进行双向对话。然而,在 raw 中使用委托可能需要创建一些样板代码(定义委托、声明必要的成员变量、创建自定义注册和注销方法以保留封装,等等)。).
此外,当您使用 raw 中的委托作为应用的回调机制时,如果您没有将类的委托成员变量定义为 private,调用方将可以直接访问委托对象。在这种情况下,调用者可以将变量重新分配给一个新的委托对象(有效地删除当前要调用的函数列表),更糟糕的是,调用者可以直接调用委托的调用列表。为了演示这个问题,创建一个名为 PublicDelegateProblem 的新控制台应用,并添加对前面 CarDelegate 示例中的Car
类的以下修改(和简化):
namespace PublicDelegateproblem
{
public class Car
{
public delegate void CarEngineHandler(string msgForCaller);
// Now a public member!
public CarEngineHandler ListOfHandlers;
// Just fire out the Exploded notification.
public void Accelerate(int delta)
{
if (ListOfHandlers != null)
{
ListOfHandlers("Sorry, this car is dead...");
}
}
}
}
请注意,您不再拥有用自定义注册方法封装的私有委托成员变量。因为这些成员确实是公共的,所以调用者可以直接访问listOfHandlers
成员变量,并将该类型重新分配给新的CarEngineHandler
对象,并在需要时调用委托。
using System;
using PublicDelegateProblem;
Console.WriteLine("***** Agh! No Encapsulation! *****\n");
// Make a Car.
Car myCar = new Car();
// We have direct access to the delegate!
myCar.ListOfHandlers = CallWhenExploded;
myCar.Accelerate(10);
// We can now assign to a whole new object...
// confusing at best.
myCar.ListOfHandlers = CallHereToo;
myCar.Accelerate(10);
// The caller can also directly invoke the delegate!
myCar.ListOfHandlers.Invoke("hee, hee, hee...");
Console.ReadLine();
static void CallWhenExploded(string msg)
{
Console.WriteLine(msg);
}
static void CallHereToo(string msg)
{
Console.WriteLine(msg);
}
公开公共委托成员会破坏封装,这不仅会导致代码难以维护(和调试),还会使您的应用面临潜在的安全风险!以下是当前示例的输出:
***** Agh! No Encapsulation! *****
Sorry, this car is dead...
Sorry, this car is dead...
hee, hee, hee...
显然,您不希望让其他应用有权更改委托所指向的内容,或者在未经您允许的情况下调用成员。鉴于此,通常的做法是声明私有委托成员变量。
C# 事件关键字
作为一种快捷方式,C# 提供了event
关键字,这样您就不必构建自定义方法来向委托的调用列表添加或移除方法。当编译器处理event
关键字时,会自动为您提供注册和注销方法,以及您的委托类型所需的任何成员变量。这些委托成员变量总是声明为私有的,因此,它们不会直接从触发事件的对象中暴露出来。当然,event
关键字可以用来简化定制类向外部对象发送通知的方式。
定义事件是一个两步过程。首先,您需要定义一个委托类型(或者重用一个现有的类型),它将保存事件触发时要调用的方法列表。接下来,根据相关的委托类型声明一个事件(使用 C# event
关键字)。
为了说明event
关键字,创建一个名为 CarEvents 的新控制台应用。在这个Car
类的迭代中,您将定义两个名为AboutToBlow
和Exploded
的事件。这些事件与一个名为CarEngineHandler
的委托类型相关联。下面是对Car
类的初始更新:
using System;
namespace CarEvents
{
public class Car
{
...
// This delegate works in conjunction with the
// Car's events.
public delegate void CarEngineHandler(string msgForCaller);
// This car can send these events.
public event CarEngineHandler Exploded;
public event CarEngineHandler AboutToBlow;
...
}
}
向调用者发送事件非常简单,只需按名称指定事件,以及相关委托定义的任何必需参数。为了确保调用者确实注册了事件,在调用委托的方法集之前,您需要对照一个null
值来检查事件。考虑到这几点,下面是Car
的Accelerate()
方法的新迭代:
public void Accelerate(int delta)
{
// If the car is dead, fire Exploded event.
if (_carIsDead)
{
Exploded?.Invoke("Sorry, this car is dead...");
}
else
{
CurrentSpeed += delta;
// Almost dead?
if (10 == MaxSpeed - CurrentSpeed)
{
AboutToBlow?.Invoke("Careful buddy! Gonna blow!");
}
// Still OK!
if (CurrentSpeed >= MaxSpeed)
{
_carIsDead = true;
}
else
{
Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed);
}
}
}
至此,您已经配置了 car 来发送两个定制事件,而不必定义定制注册函数或声明委托成员变量。您将很快看到这种新汽车的用法,但首先让我们更详细地检查一下事件架构。
幕后事件
当编译器处理 C# event
关键字时,它会生成两个隐藏方法,一个有一个add_
前缀,另一个有一个remove_
前缀。每个前缀后跟 C# 事件的名称。例如,Exploded
事件产生了两个名为add_Exploded()
和remove_Exploded()
的隐藏方法。如果您要查看add_AboutToBlow()
后面的 CIL 指令,您会发现对Delegate.Combine()
方法的调用。考虑部分 CIL 码:
.method public hidebysig specialname instance void add_AboutToBlow(
class [System.Runtime]System.EventHandler`1<class CarEvents.CarEventArgs> 'value') cil managed
{
...
IL_000b: call class [System.Runtime]System.Delegate [System.Runtime]System.Delegate::Combine(class [System.Runtime]System.Delegate, class [System.Runtime]System.Delegate)
...
} // end of method Car::add_AboutToBlow
正如您所料,remove_AboutToBlow()
将代表您调用Delegate.Remove()
。
.method public hidebysig specialname instance void remove_AboutToBlow (
class [System.Runtime]System.EventHandler`1<class CarEvents.CarEventArgs> 'value') cil managed
{
...
IL_000b: call class [System.Runtime]System.Delegate [System.Runtime]System.Delegate::Remove(class [System.Runtime]System.Delegate, class [System.Runtime]System.Delegate)
...
}
最后,代表事件本身的 CIL 代码使用.addon
和.removeon
指令来映射要调用的正确的add_XXX()
和remove_XXX()
方法的名称。
.event class [System.Runtime]System.EventHandler`1<class CarEvents.CarEventArgs> AboutToBlow
{
.addon instance void CarEvents.Car::add_AboutToBlow(
class [System.Runtime]System.EventHandler`1<class CarEvents.CarEventArgs>)
.removeon instance void CarEvents.Car::remove_AboutToBlow(
class [System.Runtime]System.EventHandler`1<class CarEvents.CarEventArgs>)
} // end of event Car::AboutToBlow
既然您已经理解了如何构建一个可以发送 C# 事件的类(并且意识到事件只不过是一个节省键入时间的工具),下一个大问题就是如何在调用者端监听传入的事件。
监听传入事件
C# 事件还简化了注册调用方事件处理程序的操作。调用者不必指定定制的助手方法,而是直接使用+=
和-=
操作符(这将在后台触发正确的add_XXX()
或remove_XXX()
方法)。当您想要注册某个事件时,请遵循此处显示的模式:
// NameOfObject.NameOfEvent +=
// new RelatedDelegate(functionToCall);
//
Car.CarEngineHandler d =
new Car.CarEngineHandler(CarExplodedEventHandler);
myCar.Exploded += d;
当您想要从事件源分离时,使用-=
操作符,使用以下模式:
// NameOfObject.NameOfEvent -=
// new RelatedDelegate(functionToCall);
//
myCar.Exploded -= d;
请注意,您也可以对事件使用方法组转换语法:
Car.CarEngineHandler d = CarExplodedEventHandler;
myCar.Exploded += d;
给定这些非常可预测的模式,下面是重构后的calling code
,现在使用 C# 事件注册语法:
Console.WriteLine("***** Fun with Events *****\n");
Car c1 = new Car("SlugBug", 100, 10);
// Register event handlers.
c1.AboutToBlow += CarIsAlmostDoomed;
c1.AboutToBlow += CarAboutToBlow;
Car.CarEngineHandler d = CarExploded;
c1.Exploded += d;
Console.WriteLine("***** Speeding up *****");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
// Remove CarExploded method
// from invocation list.
c1.Exploded -= d;
Console.WriteLine("\n***** Speeding up *****");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();
static void CarAboutToBlow(string msg)
{
Console.WriteLine(msg);
}
static void CarIsAlmostDoomed(string msg)
{
Console.WriteLine("=> Critical Message from Car: {0}", msg);
}
static void CarExploded(string msg)
{
Console.WriteLine(msg);
}
使用 Visual Studio 简化事件注册
Visual Studio 帮助注册事件处理程序。当您在事件注册期间应用+=
语法时,您会发现显示了一个智能感知窗口,邀请您点击 Tab 键来自动完成关联的委托实例(参见图 12-1 ,它是使用方法组转换语法捕获的。
图 12-1。
委托选择智能感知
点击 Tab 键后,IDE 会自动生成新方法,如图 12-2 所示。
图 12-2。
委托目标格式智能感知
注意存根代码是委托目标的正确格式(注意这个方法已经被声明为静态的,因为事件是在静态方法中注册的)。
static void NewCar_AboutToBlow(string msg)
{
throw new NotImplementedException();
}
所有人都可以使用智能感知。NET 核心事件、自定义事件和基类库中的所有事件。这个 IDE 特性可以节省大量的时间,因为它使您不必搜索帮助系统来确定事件要使用的正确委托以及委托目标方法的格式。
创建自定义事件参数
说实话,你可以对当前的Car
类做最后一个增强,它反映了微软推荐的事件模式。当你开始研究基类库中给定类型发送的事件时,你会发现底层委托的第一个参数是一个System.Object
,而第二个参数是System.EventArgs
的后代。
System.Object
参数表示对发送事件的对象的引用(比如Car
),而第二个参数表示关于当前事件的信息。System.EventArgs
基类表示不发送任何自定义信息的事件。
public class EventArgs
{
public static readonly EventArgs Empty;
public EventArgs();
}
对于简单的事件,可以直接传递一个EventArgs
的实例。然而,当您想要传递自定义数据时,您应该构建一个从EventArgs
派生的合适的类。对于这个例子,假设您有一个名为CarEventArgs
的类,它维护一个表示发送给接收者的消息的字符串。
using System;
namespace CarEvents
{
public class CarEventArgs : EventArgs
{
public readonly string msg;
public CarEventArgs(string message)
{
msg = message;
}
}
}
这样,您现在可以如下更新CarEngineHandler
委托类型定义(事件将保持不变):
public class Car
{
public delegate void CarEngineHandler(object sender, CarEventArgs e);
...
}
这里,当从Accelerate()
方法中触发事件时,您现在需要提供一个对当前Car
(通过this
关键字)的引用和一个CarEventArgs
类型的实例。例如,考虑以下部分更新:
public void Accelerate(int delta)
{
// If the car is dead, fire Exploded event.
if (carIsDead)
{
Exploded?.Invoke(this, new CarEventArgs("Sorry, this car is dead..."));
}
...
}
在调用者端,您需要做的就是更新您的事件处理程序来接收传入的参数并通过只读字段获取消息。这里有一个例子:
static void CarAboutToBlow(object sender, CarEventArgs e)
{
Console.WriteLine($"{sender} says: {e.msg}");
}
如果接收者想要与发送事件的对象交互,可以显式地强制转换System.Object
。从这个引用中,您可以利用发送事件通知的对象的任何公共成员。
static void CarAboutToBlow(object sender, CarEventArgs e)
{
// Just to be safe, perform a
// runtime check before casting.
if (sender is Car c)
{
Console.WriteLine(
$"Critical Message from {c.PetName}: {e.msg}");
}
}
通用 EventHandler 委托
鉴于如此多的自定义委托将一个object
作为第一个参数,将一个EventArgs
后代作为第二个参数,您可以通过使用通用的EventHandler<T>
类型来进一步简化前面的示例,其中T
是您的自定义EventArgs
类型。考虑下面对Car
类型的更新(注意您不再需要定义自定义委托类型):
public class Car
{
...
public event EventHandler<CarEventArgs> Exploded;
public event EventHandler<CarEventArgs> AboutToBlow;
}
然后,调用代码可以在之前指定了CarEventHandler
的任何地方使用EventHandler<CarEventArgs>
(或者,再次使用方法组转换)。
Console.WriteLine("***** Prim and Proper Events *****\n");
// Make a car as usual.
Car c1 = new Car("SlugBug", 100, 10);
// Register event handlers.
c1.AboutToBlow += CarIsAlmostDoomed;
c1.AboutToBlow += CarAboutToBlow;
EventHandler<CarEventArgs> d = CarExploded;
c1.Exploded += d;
...
太好了。至此,您已经看到了在 C# 语言中使用委托和事件的核心方面。虽然您可以使用这些信息来满足所有的回调需求,但是在本章结束时,您将会看到一些最终的简化,特别是匿名方法和 lambda 表达式。
了解 C# 匿名方法
正如您所看到的,当调用者想要监听传入事件时,它必须在一个类(或结构)中定义一个自定义方法,该方法与相关委托的签名相匹配。这里有一个例子:
SomeType t = new SomeType();
// Assume "SomeDelegate" can point to methods taking no
// args and returning void.
t.SomeEvent += new SomeDelegate(MyEventHandler);
// Typically only called by the SomeDelegate object.
static void MyEventHandler()
{
// Do something when event is fired.
}
然而,仔细想想,像MyEventHandler()
这样的方法很少会被程序中除了调用委托之外的任何部分调用。就生产效率而言,手动定义一个由委托对象调用的单独方法有点麻烦(尽管这绝不是一个阻碍)。
为了解决这一点,可以在事件注册时将事件直接关联到代码语句块。形式上,这样的代码被称为匿名方法。为了说明语法,首先创建一个名为 AnonymousMethods 的新控制台应用,并将 CarEvents 项目中的Car.cs
和CarEventArgs.cs
类复制到新项目中(确保将它们的名称空间更改为AnonymousMethods
)。更新Program.cs
文件的代码以匹配下面的代码,它使用匿名方法处理从Car
类发送的事件,而不是专门命名的事件处理程序:
using System;
using AnonymousMethods;
Console.WriteLine("***** Anonymous Methods *****\n");
Car c1 = new Car("SlugBug", 100, 10);
// Register event handlers as anonymous methods.
c1.AboutToBlow += delegate
{
Console.WriteLine("Eek! Going too fast!");
};
c1.AboutToBlow += delegate(object sender, CarEventArgs e)
{
Console.WriteLine("Message from Car: {0}", e.msg);
};
c1.Exploded += delegate(object sender, CarEventArgs e)
{
Console.WriteLine("Fatal Message from Car: {0}", e.msg);
};
// This will eventually trigger the events.
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();
Note
匿名方法的最后一个花括号必须以分号结束。如果不这样做,就会出现编译错误。
再次注意,调用代码不再需要定义特定的静态事件处理程序,比如CarAboutToBlow()
或CarExploded()
。相反,在调用者使用+=
语法处理事件时,未命名(又名匿名)方法被内联定义。匿名方法的基本语法与以下伪代码相匹配:
SomeType t = new SomeType();
t.SomeEvent += delegate (optionallySpecifiedDelegateArgs)
{ /* statements */ };
当处理前一个代码示例中的第一个AboutToBlow
事件时,请注意,您没有指定从委托传递的参数。
c1.AboutToBlow += delegate
{
Console.WriteLine("Eek! Going too fast!");
};
严格地说,您不需要接收特定事件发送的传入参数。但是,如果您想要利用可能的传入参数,您将需要指定由委托类型原型化的参数(如第二个对AboutToBlow
和Exploded
事件的处理所示)。这里有一个例子:
c1.AboutToBlow += delegate(object sender, CarEventArgs e)
{
Console.WriteLine("Critical Message from Car: {0}", e.msg);
};
访问局部变量
匿名方法很有趣,因为它们可以访问定义它们的方法的局部变量。从形式上讲,这样的变量被称为匿名方法的外部变量。关于匿名方法范围和定义方法范围之间的交互,应该提到以下要点:
-
匿名方法不能访问定义方法的
ref
或out
参数。 -
匿名方法中的局部变量不能与外部方法中的局部变量同名。
-
匿名方法可以访问外部类范围内的实例变量(或静态变量,视情况而定)。
-
匿名方法可以声明与外部类成员变量同名的局部变量(局部变量具有不同的范围并隐藏外部类成员变量)。
假设您的顶级语句定义了一个名为aboutToBlowCounter
的局部整数。在处理AboutToBlow
事件的匿名方法中,您将使这个计数器加 1,并在语句完成之前打印出计数。
Console.WriteLine("***** Anonymous Methods *****\n");
int aboutToBlowCounter = 0;
// Make a car as usual.
Car c1 = new Car("SlugBug", 100, 10);
// Register event handlers as anonymous methods.
c1.AboutToBlow += delegate
{
aboutToBlowCounter++;
Console.WriteLine("Eek! Going too fast!");
};
c1.AboutToBlow += delegate(object sender, CarEventArgs e)
{
aboutToBlowCounter++;
Console.WriteLine("Critical Message from Car: {0}", e.msg);
};
...
// This will eventually trigger the events.
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.WriteLine("AboutToBlow event was fired {0} times.",
aboutToBlowCounter);
Console.ReadLine();
运行更新后的代码后,您会发现最后的Console.WriteLine()
报告了AboutToBlow
事件被触发了两次。
使用静态和匿名方法(新 9.0)
前面的例子演示了匿名方法与方法本身范围之外声明的变量进行交互。虽然这可能是您想要的,但它破坏了封装,并可能在您的程序中引入意想不到的副作用。回想一下第四章,通过将局部函数设置为静态,可以将它们从包含代码中分离出来,如下例所示:
static int AddWrapperWithStatic(int x, int y)
{
//Do some validation here
return Add(x,y);
static int Add(int x, int y)
{
return x + y;
}
}
C# 9.0 中新增的匿名方法也可以标记为静态,以保持封装性,并确保该方法不会给包含它的代码带来任何副作用。例如,请参见此处更新的匿名方法:
c1.AboutToBlow += static delegate
{
//This causes a compile error because it is marked static
aboutToBlowCounter++;
Console.WriteLine("Eek! Going too fast!");
};
由于匿名方法试图访问在其范围之外声明的变量,上述代码将无法编译。
用匿名方法丢弃(新 9.0)
在第三章中介绍的丢弃,已经在 C# 9.0 中更新,作为匿名方法的输入参数,带有一个 catch。因为下划线(_
)在以前版本的 C# 中是一个合法的变量标识符,所以必须有两个或更多与匿名方法一起使用的丢弃才会被视为丢弃。
例如,下面的代码为一个接受两个整数并返回另一个整数的Func
创建了一个委托。该实现忽略任何传入的变量,并返回 42:
Console.WriteLine("******** Discards with Anonymous Methods ********");
Func<int,int,int> constant = delegate (int _, int _) {return 42;};
Console.WriteLine("constant(3,4)={0}",constant(3,4));
理解 Lambda 表达式
以此来结束您对。NET 核心事件架构,您将研究 C# lambda 表达式。正如刚才所解释的,C# 支持“内联”处理事件的能力,方法是使用匿名方法将一组代码语句直接分配给一个事件,而不是构建一个由底层委托调用的独立方法。Lambda 表达式只不过是一种简洁的方式来创作匿名方法,并最终简化您使用。NET 核心委托类型。
要为 lambda 表达式的检查做准备,请创建一个名为 lambda expressions 的新控制台应用项目。首先,考虑泛型List<T>
类的FindAll()
方法。当您需要从集合中提取项目的子集时,可以调用此方法,其原型如下:
// Method of the System.Collections.Generic.List<T>
public List<T> FindAll(Predicate<T> match)
正如您所看到的,这个方法返回了一个新的代表数据子集的List<T>
。还要注意的是,FindAll()
的唯一参数是一个类型为System.Predicate<T>
的泛型委托。这个委托类型可以指向任何返回一个bool
的方法,并将一个类型参数作为唯一的输入参数。
// This delegate is used by FindAll() method
// to extract out the subset.
public delegate bool Predicate<T>(T obj);
当你调用FindAll()
时,List<T>
中的每一项都被传递给Predicate<T>
对象所指向的方法。所述方法的实现将执行一些计算,以查看传入的数据是否匹配必要的标准,并将返回true
或false
。如果这个方法返回true
,这个条目将被添加到新的代表子集的List<T>
中(明白了吗?).
在您看到 lambda 表达式如何简化使用FindAll()
之前,让我们直接使用委托对象,用手写符号来解决这个问题。在您的Program
类型中添加一个与System.Predicate<T>
类型交互的方法(名为TraditionalDelegateSyntax()
),以发现整数的List<T>
中的偶数。
using System;
using System.Collections.Generic;
using LambdaExpressions;
Console.WriteLine("***** Fun with Lambdas *****\n");
TraditionalDelegateSyntax();
Console.ReadLine();
static void TraditionalDelegateSyntax()
{
// Make a list of integers.
List<int> list = new List<int>();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });
// Call FindAll() using traditional delegate syntax.
Predicate<int> callback = IsEvenNumber;
List<int> evenNumbers = list.FindAll(callback);
Console.WriteLine("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}
// Target for the Predicate<> delegate.
static bool IsEvenNumber(int i)
{
// Is it an even number?
return (i % 2) == 0;
}
这里,您有一个方法(IsEvenNumber()
),它通过 C# 模操作符%
监督测试传入的整数参数,以查看它是偶数还是奇数。如果您执行您的应用,您会发现数字 20、4、8 和 44 打印到控制台。
虽然这种使用委托的传统方法如预期的那样工作,但是只有在有限的情况下才会调用IsEvenNumber()
方法——特别是当您调用FindAll()
时,这会给您留下一个完整方法定义的包袱。虽然您可以将它作为一个局部函数,但是如果您使用匿名方法,您的代码将会清理得相当干净。考虑下面这个Program
类的新方法:
static void AnonymousMethodSyntax()
{
// Make a list of integers.
List<int> list = new List<int>();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });
// Now, use an anonymous method.
List<int> evenNumbers =
list.FindAll(delegate(int i) { return (i % 2) == 0; } );
Console.WriteLine("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}
在这种情况下,您可以匿名内联一个方法,而不是直接创建一个Predicate<T>
委托对象,然后创作一个独立的方法。虽然这是朝着正确方向迈出的一步,但是仍然需要使用关键字delegate
(或者强类型的Predicate<T>
),并且必须确保参数列表是完全匹配的。
List<int> evenNumbers = list.FindAll(
delegate(int i)
{
return (i % 2) == 0;
}
);
λ表达式可以用来进一步简化对FindAll()
的调用。当您使用 lambda 语法时,根本没有任何底层委托对象的痕迹。考虑下面对Program
类的新方法:
static void LambdaExpressionSyntax()
{
// Make a list of integers.
List<int> list = new List<int>();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });
// Now, use a C# lambda expression.
List<int> evenNumbers = list.FindAll(i => (i % 2) == 0);
Console.WriteLine("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}
在这种情况下,请注意传递给FindAll()
方法的代码语句,它实际上是一个 lambda 表达式。在这个例子的迭代中,没有任何关于Predicate<T>
委托(或者delegate
关键字)的痕迹。您所指定的只是 lambda 表达式。
i => (i % 2) == 0
在我分解这个语法之前,首先要理解 lambda 表达式可以用在任何使用匿名方法或强类型委托的地方(通常击键次数少得多)。在幕后,C# 编译器利用Predicate<T>
委托类型(可以使用ildasm.exe
或reflector.exe
来验证)将表达式翻译成标准的匿名方法。具体来说,下面的代码语句:
// This lambda expression...
List<int> evenNumbers = list.FindAll(i => (i % 2) == 0);
被编译成如下近似的 C# 代码:
// ...becomes this anonymous method.
List<int> evenNumbers = list.FindAll(delegate (int i)
{
return (i % 2) == 0;
});
剖析 Lambda 表达式
lambda 表达式是这样编写的:首先定义一个参数列表,然后是=>
标记(在 lambda 演算中找到的 lambda 运算符的 C# 标记),然后是一组将处理这些参数的语句(或单个语句)。从高层次来看,lambda 表达式可以理解为:
ArgumentsToProcess => StatementsToProcessThem
在LambdaExpressionSyntax()
方法中,事情是这样分解的:
// "i" is our parameter list.
// "(i % 2) == 0" is our statement set to process "i".
List<int> evenNumbers = list.FindAll(i => (i % 2) == 0);
lambda 表达式的参数可以显式或隐式类型化。目前,代表i
参数(整数)的底层数据类型是隐式确定的。编译器可以根据 lambda 表达式和底层委托的上下文判断出i
是一个整数。但是,也可以通过将数据类型和变量名放在一对括号中来显式定义表达式中每个参数的类型,如下所示:
// Now, explicitly state the parameter type.
List<int> evenNumbers = list.FindAll((int i) => (i % 2) == 0);
正如你所看到的,如果一个 lambda 表达式只有一个隐式类型的参数,那么圆括号可以从参数列表中省略。如果你想在 lambda 参数的使用上保持一致,你可以总是将参数列表放在括号内,留给你这个表达式:
List<int> evenNumbers = list.FindAll((i) => (i % 2) == 0);
最后,请注意,当前表达式没有用括号括起来(当然,您已经将 modulo 语句括起来,以确保它在相等测试之前首先执行)。Lambda 表达式允许语句包装如下:
// Now, wrap the expression as well.
List<int> evenNumbers = list.FindAll((i) => ((i % 2) == 0));
既然您已经看到了构建 lambda 表达式的各种方法,那么您如何以人类友好的方式阅读这个 lambda 语句呢?抛开原始的数学,下面的解释符合这个要求:
// My list of parameters (in this case, a single integer named i)
// will be processed by the expression (i % 2) == 0.
List<int> evenNumbers = list.FindAll((i) => ((i % 2) == 0));
处理多条语句中的参数
第一个 lambda 表达式是一个最终计算为布尔值的语句。然而,如您所知,许多委托目标必须执行几个代码语句。出于这个原因,C# 允许您通过使用标准花括号指定代码块来构建包含多个语句的 lambda 表达式。考虑以下对LambdaExpressionSyntax()
方法的示例更新:
static void LambdaExpressionSyntax()
{
// Make a list of integers.
List<int> list = new List<int>();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });
// Now process each argument within a group of
// code statements.
List<int> evenNumbers = list.FindAll((i) =>
{
Console.WriteLine("value of i is currently: {0}", i);
bool isEven = ((i % 2) == 0);
return isEven;
});
Console.WriteLine("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}
在这种情况下,参数列表(同样是一个名为i
的整数)由一组代码语句处理。除了对Console.WriteLine()
的调用,为了增加可读性,modulo 语句被分成了两个代码语句。假设您在本节中看到的每个方法都是从顶级语句中调用的:
Console.WriteLine("***** Fun with Lambdas *****\n");
TraditionalDelegateSyntax();
AnonymousMethodSyntax();
Console.WriteLine();
LambdaExpressionSyntax();
Console.ReadLine();
您会发现以下输出:
***** Fun with Lambdas *****
Here are your even numbers:
20 4 8 44
Here are your even numbers:
20 4 8 44
value of i is currently: 20
value of i is currently: 1
value of i is currently: 4
value of i is currently: 8
value of i is currently: 9
value of i is currently: 44
Here are your even numbers:
20 4 8 44
具有多个(或零个)参数的 Lambda 表达式
到目前为止,你在本章中看到的 lambda 表达式只处理了一个参数。然而,这不是必需的,因为 lambda 表达式可以处理多个参数(或者一个都不处理)。为了说明多参数的第一种情况,添加下面的SimpleMath
类型实例:
public class SimpleMath
{
public delegate void MathMessage(string msg, int result);
private MathMessage _mmDelegate;
public void SetMathHandler(MathMessage target)
{
_mmDelegate = target;
}
public void Add(int x, int y)
{
_mmDelegate?.Invoke("Adding has completed!", x + y);
}
}
请注意,MathMessage
委托类型需要两个参数。为了将它们表示为 lambda 表达式,可以将Main()
方法编写如下:
// Register with delegate as a lambda expression.
SimpleMath m = new SimpleMath();
m.SetMathHandler((msg, result) =>
{Console.WriteLine("Message: {0}, Result: {1}", msg, result);});
// This will execute the lambda expression.
m.Add(10, 10);
Console.ReadLine();
这里,您利用了类型推断,因为为了简单起见,这两个参数没有被强类型化。但是,您可以调用SetMathHandler()
,如下所示:
m.SetMathHandler((string msg, int result) =>
{Console.WriteLine("Message: {0}, Result: {1}", msg, result);});
最后,如果使用 lambda 表达式与不带任何参数的委托进行交互,可以通过提供一对空括号作为参数来实现。因此,假设您已经定义了以下委托类型:
public delegate string VerySimpleDelegate();
您可以按如下方式处理调用的结果:
// Prints "Enjoy your string!" to the console.
VerySimpleDelegate d = new VerySimpleDelegate( () => {return "Enjoy your string!";} );
Console.WriteLine(d());
使用新的表达式语法,前一行可以写成这样:
VerySimpleDelegate d2 =
new VerySimpleDelegate(() => "Enjoy your string!");
也可以简化为:
VerySimpleDelegate d3 = () => "Enjoy your string!";
在 Lambda 表达式中使用 static(新 9.0)
因为 lambda 表达式是委托的简写,所以可以理解 lambda 也支持static
关键字(在 C# 9.0 中)和丢弃(在下一节讨论)。将以下内容添加到顶级语句中:
var outerVariable = 0;
Func<int, int, bool> DoWork = (x,y) =>
{
outerVariable++;
return true;
};
DoWork(3,4);
Console.WriteLine("Outer variable now = {0}", outerVariable);
执行此代码时,它会输出以下内容:
***** Fun with Lambdas *****
Outer variable now = 1
如果将 lambda 更新为 static,将会收到一个编译错误,因为表达式试图更新外部作用域中声明的变量。
var outerVariable = 0;
Func<int, int, bool> DoWork = static (x,y) =>
{
//Compile error since it’s accessing an outer variable
//outerVariable++;
return true;
};
用 Lambda 表达式丢弃(新 9.0)
与委托(和 C# 9.0)一样,如果不需要输入变量,lambda 表达式的输入变量可以用丢弃来替换。与委托的情况相同。因为下划线(_)在以前版本的 C# 中是合法的变量标识符,所以它们必须在 lambda 表达式中被丢弃两次或更多次。
var outerVariable = 0;
Func<int, int, bool> DoWork = (x,y) =>
{
outerVariable++;
return true;
};
DoWork(_,_);
Console.WriteLine("Outer variable now = {0}", outerVariable);
使用 lambda 表达式改进汽车事件示例
考虑到 lambda 表达式的全部原因是提供一种干净、简洁的方式来定义一个匿名方法(从而间接地简化委托的工作),让我们改进本章前面创建的CarEventArgs
项目。下面是该项目的Program
类的简化版本,它利用 lambda 表达式语法(而不是原始委托)来挂钩从Car
对象发送的每个事件:
using System;
using CarEventsWithLambdas;
Console.WriteLine("***** More Fun with Lambdas *****\n");
// Make a car as usual.
Car c1 = new Car("SlugBug", 100, 10);
// Hook into events with lambdas!
c1.AboutToBlow += (sender, e)
=> { Console.WriteLine(e.msg);};
c1.Exploded += (sender, e) => { Console.WriteLine(e.msg); };
// Speed up (this will generate the events).
Console.WriteLine("\n***** Speeding up *****");
for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();
Lambdas 和 Expression-body 成员(更新 7.0)
既然您已经理解了 lambda 表达式及其工作原理,那么显而易见的是表达式体成员是如何工作的。正如第四章提到的,从 C# 6 开始,允许使用=>
操作符来简化成员实现。具体来说,如果您有一个方法或属性(除了自定义运算符或转换例程之外;参见第十一章)在实现中只包含一行代码,您不需要通过花括号定义作用域。相反,您可以利用 lambda 运算符并编写一个表达式体成员。在 C# 7 中,你也可以对类构造器、终结器(在第九章中讨论)以及属性成员的get
和set
访问器使用这种语法。
但是,请注意,这种新的简化语法可以在任何地方使用,即使您的代码与委托或事件无关。因此,例如,如果您要构建一个简单的类来添加两个数字,您可以编写以下代码:
class SimpleMath
{
public int Add(int x, int y)
{
return x + y;
}
public void PrintSum(int x, int y)
{
Console.WriteLine(x + y);
}
}
或者,您现在可以编写如下代码:
class SimpleMath
{
public int Add(int x, int y) => x + y;
public void PrintSum(int x, int y) => Console.WriteLine(x + y);
}
理想情况下,在这一点上,您可以看到 lambda 表达式的整体作用,并理解它们如何提供一种“函数方式”来处理匿名方法和委托类型。尽管 lambda 运算符(=>
)可能需要一点时间来适应,但请记住,lambda 表达式可以分解为以下简单的等式:
ArgumentsToProcess =>
{
//StatementsToProcessThem
}
或者,如果使用=>
操作符来实现单行类型成员,它将是这样的:
TypeMember => SingleCodeStatement
值得指出的是,LINQ 编程模型也大量使用 lambda 表达式来帮助简化您的编码工作。你将从第十三章开始研究 LINQ。
摘要
在本章中,您研究了多个对象参与双向对话的几种方式。首先,您查看了 C# delegate
关键字,该关键字用于间接构造从System.MulticastDelegate
派生的类。如您所见,委托对象在被告知调用方法时会维护该方法。
然后研究了 C# event
关键字,当它与委托类型结合使用时,可以简化将事件通知发送给等待调用方的过程。如生成的 CIL 所示。NET 事件模型映射到System.Delegate
/ System.MulticastDelegate
类型的隐藏调用。在这种情况下,C# event
关键字完全是可选的,因为它只是为您节省了一些键入时间。同样,您已经看到 C# 6.0 空条件操作符简化了您如何安全地向任何感兴趣的一方触发事件。
本章还探讨了 C# 语言的一个特性,称为匿名方法。使用这种语法结构,您可以将代码语句块直接关联到给定的事件。正如您所看到的,匿名方法可以忽略事件发送的参数,并可以访问定义方法的“外部变量”。您还研究了使用方法组转换注册事件的简化方法。
最后,通过查看 C# lambda 操作符、=>
来总结一下。如图所示,这种语法是创作匿名方法的一种很好的速记符号,其中可以将一堆参数传递给一组语句进行处理。中的任何方法。NET 核心平台可以用一个相关的 lambda 表达式来代替,这通常会大大简化你的代码库。
十三、LINQToObj
无论您使用。NET 核心平台,您的程序在执行时肯定需要访问某种形式的数据。可以肯定的是,数据可以在很多地方找到,包括 XML 文件、关系数据库、内存集合和原始数组。从历史上来说,基于所述数据的位置,程序员需要使用不同的和不相关的 API。语言集成查询(LINQ)技术集,最初在。NET 3.5 提供了一种简洁、对称和强类型的方式来访问各种各样的数据存储。在这一章中,你将通过关注 LINQ 来开始你对 LINQ 的调查。
在深入 LINQ 到对象本身之前,本章的第一部分快速回顾了支持 LINQ 的关键 C# 编程结构。当你阅读本章时,你会发现隐式类型的局部变量、对象初始化语法、lambda 表达式、扩展方法和匿名类型将会非常有用(如果不是偶尔强制的话)。
在回顾了这个支持基础结构之后,本章的剩余部分将向您介绍 LINQ 编程模型及其在。NET 平台。在这里,您将学习查询操作符和查询表达式的作用,它们允许您定义查询数据源以产生请求的结果集的语句。在这个过程中,您将构建许多与数组中包含的数据以及各种集合类型(泛型和非泛型)进行交互的 LINQ 示例,并理解表示 LINQ 到对象 API 的程序集、命名空间和类型。
Note
本章中的信息是本书以后章节的基础,包括并行 LINQ(第十五章)和实体框架核心(第 22 和 23 章)。
特定于 LINQ 的编程结构
从高层次来看,LINQ 可以理解为一种强类型查询语言,直接嵌入到 C# 的语法中。使用 LINQ,您可以构建任意数量的表达式,其外观和感觉都像数据库 SQL 查询。然而,LINQ 查询可以应用于任何数量的数据存储,包括与文字关系数据库无关的存储。
Note
尽管 LINQ 查询看起来类似于 SQL 查询,但是语法是不相同的。事实上,许多 LINQ 查询似乎是一个类似的数据库查询的完全相反的格式!如果您试图将 LINQ 直接映射到 SQL,您肯定会感到沮丧。为了保持理智,我建议您尽最大努力将 LINQ 查询视为唯一的语句,它只是“碰巧看起来”像 SQL。
当 LINQ 第一次被介绍给。NET 平台的 3.5 版本中,C# 和 VB 语言都扩展了许多新的编程结构,用于支持 LINQ 技术集。具体来说,C# 语言使用以下以 LINQ 为中心的核心功能:
-
隐式类型的局部变量
-
对象/集合初始化语法
-
λ表达式
-
扩展方法
-
匿名类型
这些特征已经在文本的各个章节中详细探讨过了。然而,为了开始,让我们快速地依次回顾一下每个特性,以确保我们都处于正确的心态。
Note
因为接下来的部分是对本书其他地方的内容的回顾,所以我没有为这些内容包含 C# 代码项目。
局部变量的隐式类型化
在第三章中,你学习了 C# 的var
关键字。该关键字允许您定义局部变量,而无需显式指定基础数据类型。但是,该变量是强类型的,因为编译器将根据初始赋值确定正确的数据类型。回想一下第三章中的代码示例:
static void DeclareImplicitVars()
{
// Implicitly typed local variables.
var myInt = 0;
var myBool = true;
var myString = "Time, marches on...";
// Print out the underlying type.
Console.WriteLine("myInt is a: {0}", myInt.GetType().Name);
Console.WriteLine("myBool is a: {0}",
myBool.GetType().Name);
Console.WriteLine("myString is a: {0}",
myString.GetType().Name);
}
在使用 LINQ 时,这种语言特性很有帮助,而且通常是强制性的。正如您将在本章中看到的,许多 LINQ 查询将返回一系列数据类型,这些数据类型直到编译时才知道。考虑到在编译应用之前不知道底层数据类型,显然不能显式声明变量!
对象和集合初始化语法
第五章探讨了对象初始化语法的作用,它允许你创建一个类或结构变量,并一次性设置任意数量的公共属性。结果是一个紧凑的(但仍然很容易看)语法,可以用来让您的对象准备好使用。还记得第九章的内容吗,C# 语言允许你使用类似的语法来初始化对象集合。考虑下面的代码片段,它使用集合初始化语法来填充一个Rectangle
对象的List<T>
,每个对象维护两个Point
对象来表示一个(x,y)位置:
List<Rectangle> myListOfRects = new List<Rectangle>
{
new Rectangle {TopLeft = new Point { X = 10, Y = 10 },
BottomRight = new Point { X = 200, Y = 200}},
new Rectangle {TopLeft = new Point { X = 2, Y = 2 },
BottomRight = new Point { X = 100, Y = 100}},
new Rectangle {TopLeft = new Point { X = 5, Y = 5 },
BottomRight = new Point { X = 90, Y = 75}}
};
虽然您从来不需要使用集合/对象初始化语法,但是这样做可以产生更紧凑的代码库。此外,当与局部变量的隐式类型化结合使用时,这种语法允许您声明一个匿名类型,这在创建 LINQ 投影时很有用。在本章的后面你会学到 LINQ 投影。
λ表达式
C# lambda 操作符(=>
)在第十二章中有充分的探讨。回想一下,这个操作符允许您构建 lambda 表达式,只要您调用需要强类型委托作为参数的方法,就可以使用这个表达式。Lambdas 极大地简化了您使用委托的方式,因为它们减少了您必须手工创作的代码量。回想一下,lambda 表达式可以分解为以下用法:
( ArgumentsToProcess ) => { StatementsToProcessThem }
在第十二章中,我用三种不同的方法向你展示了如何与泛型List<T>
类的FindAll()
方法进行交互。在使用了原始的Predicate<T>
委托和一个 C# 匿名方法之后,您最终用这个 lambda 表达式实现了下面这个(非常简洁的)迭代:
static void LambdaExpressionSyntax()
{
// Make a list of integers.
List<int> list = new List<int>();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });
// C# lambda expression.
List<int> evenNumbers = list.FindAll(i => (i % 2) == 0);
Console.WriteLine("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}
当使用 LINQ 的底层对象模型时,Lambdas 会很有用。您很快就会发现,C# LINQ 查询操作符只是一种在名为System.Linq.Enumerable
的类上调用可靠方法的简写符号。这些方法通常需要委托(特别是Func<>
委托)作为参数,用于处理数据以产生正确的结果集。使用 lambdas,您可以简化代码,并允许编译器推断底层委托。
扩展方法
C# 扩展方法允许你在现有的类上添加新的功能,而不需要子类化。此外,扩展方法允许您向密封的类和结构添加新的功能,这些功能永远不会在第一个位置被子类化。回想一下第十一章中的,当你创建一个扩展方法时,第一个参数用this
关键字限定,并标记被扩展的类型。还记得扩展方法必须总是在静态类中定义,因此也必须使用static
关键字声明。这里有一个例子:
namespace MyExtensions
{
static class ObjectExtensions
{
// Define an extension method to System.Object.
public static void DisplayDefiningAssembly(
this object obj)
{
Console.WriteLine("{0} lives here:\n\t->{1}\n", obj.GetType().Name,
Assembly.GetAssembly(obj.GetType()));
}
}
}
若要使用此扩展,应用必须导入定义该扩展的命名空间(并可能添加对外部程序集的引用)。此时,只需导入定义的名称空间和代码。
// Since everything extends System.Object, all classes and structures
// can use this extension.
int myInt = 12345678;
myInt.DisplayDefiningAssembly();
System.Data.DataSet d = new System.Data.DataSet();
d.DisplayDefiningAssembly();
当您使用 LINQ 时,您将很少需要手动构建自己的扩展方法。但是,当您创建 LINQ 查询表达式时,您将会使用微软已经定义的许多扩展方法。事实上,每个 C# LINQ 查询操作符都是对底层扩展方法进行手动调用的简写符号,通常由System.Linq.Enumerable
实用程序类定义。
匿名类型
我想快速回顾的最后一个 C# 语言特性是匿名类型,这在第十一章中已经探讨过了。通过允许编译器在编译时基于提供的一组名称-值对生成新的类定义,该特性可用于快速建模数据的“形状”。回想一下,这个类型将使用基于值的语义来组合,并且System.Object
的每个虚方法将被相应地覆盖。若要定义匿名类型,请声明一个隐式类型变量,并使用对象初始化语法指定数据的形状。
// Make an anonymous type that is composed of another.
var purchaseItem = new {
TimeBought = DateTime.Now,
ItemBought =
new {Color = "Red", Make = "Saab", CurrentSpeed = 55},
Price = 34.000};
当你想设计新形式的数据时,LINQ 经常使用匿名类型。例如,假设您有一个Person
对象的集合,并想使用 LINQ 来获得每个对象的年龄和社会保险号信息。使用 LINQ 投影,您可以允许编译器生成包含您的信息的新匿名类型。
理解 LINQ 的角色
这就结束了对 C# 语言特性的快速回顾,这些特性让 LINQ 发挥了它的魔力。然而,为什么首先有 LINQ 呢?作为软件开发人员,很难否认大量的编程时间花费在获取和操作数据上。当谈到“数据”时,很容易立即想到关系数据库中包含的信息。然而,数据的另一个流行位置是在 XML 文档或简单的文本文件中。
除了这两个常见的信息之外,还可以在许多地方找到数据。例如,假设您有一个包含 300 个整数的数组或泛型List<T>
类型,并且您想要获得一个满足给定标准的子集(例如,容器中只有奇数或偶数成员,只有质数,只有大于 50 的非重复数)。或者,您可能正在利用反射 API,并且只需要获得从一个Type
数组中的父类派生的每个类的元数据描述。事实上,数据到处都是*。*
*之前。NET 3.5 中,与各种数据交互需要程序员使用非常多样化的 API。例如,考虑一下表 13-1 ,它说明了几种用于访问各种类型数据的常见 API(我相信您可以想到许多其他的例子)。
表 13-1。
操作各种类型数据的方法
|你想要的数据
|
如何获得它
|
| — | — |
| 关系数据 | System.Data.dll
、System.Data.SqlClient.dll
等。 |
| XML 文档数据 | System.Xml.dll
|
| 元数据表 | System.Reflection
名称空间 |
| 对象集合 | System.Array
和System.Collections/System.Collections.Generic
名称空间 |
当然,这些处理数据的方法并没有错。事实上,您可以(也将会)直接使用 ADO.NET、XML 名称空间、反射服务和各种集合类型。然而,基本的问题是这些 API 中的每一个都是一个孤岛,很少提供集成。的确,可以(例如)将 ADO.NETDataSet
保存为 XML,然后通过System.Xml
名称空间操纵它,但是尽管如此,数据操纵仍然相当不对称。
LINQ API 试图提供一种一致的、对称的方式,程序员可以在广义上获取和操作“数据”。使用 LINQ,你可以在 C# 编程语言中直接创建名为的查询表达式。这些查询表达式基于许多查询操作符,这些操作符被有意设计成看起来和感觉上与 SQL 表达式相似(但不完全相同)。
然而,问题是查询表达式可以用于与多种类型的数据交互,甚至是与关系数据库无关的数据。严格地说,“LINQ”是用来描述这种整体数据访问方法的术语。但是,根据您应用 LINQ 查询的位置,您会遇到各种术语,例如:
-
对象的 LINQ:这个术语指的是对数组和集合应用 LINQ 查询的行为。
-
LINQ 到 XML :这个术语指的是使用 LINQ 操作和查询 XML 文档的行为。
-
到实体的 LINQ:LINQ 的这一方面允许您在 ADO.NET 实体框架(EF)核心 API 内使用 LINQ 查询。
-
并行 LINQ(又名 PLINQ ):这允许并行处理从 LINQ 查询返回的数据。
今天,LINQ 是世界不可分割的一部分。NET 核心基类库、托管语言和 Visual Studio 本身。
LINQ 表达式是强类型的
指出 LINQ 查询表达式(不同于传统的 SQL 语句)是强类型的也很重要。因此,C# 编译器会让你保持诚实,并确保这些表达式的语法格式良好。Visual Studio 等工具可以将元数据用于智能感知、自动完成等有用的功能。
核心 LINQ 组件
要使用 LINQ 对象,必须确保每个包含 LINQ 查询的 C# 代码文件都导入了System.Linq
名称空间。确保使用 LINQ 的每个代码文件中都有下面的using
语句:
using System.Linq;
将 LINQ 查询应用于原始数组
要开始研究对象的 LINQ,让我们构建一个将 LINQ 查询应用于各种数组对象的应用。创建一个名为 LinqOverArray 的控制台应用项目,并在名为QueryOverStrings()
的Program
类中定义一个静态 helper 方法。在这个方法中,创建一个包含大约六个您喜欢的项目的string
数组(这里,我在我的库中列出了一批视频游戏)。确保至少有两个包含数值的条目和几个包含空格的条目。
static void QueryOverStrings()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
}
现在,更新Program.cs
来调用QueryOverStrings()
。
Console.WriteLine("***** Fun with LINQ to Objects *****\n");
QueryOverStrings();
Console.ReadLine();
当您有任何数据数组时,通常会根据给定的需求提取项目的子集。也许您只想获得包含数字的子项(例如,System Shock 2、Uncharted 2 和辐射 3)、包含一定数量的字符的子项或者不包含嵌入空格的子项(例如,Morrowind 或 Daxter)。虽然您当然可以使用System.Array
类型的成员和一些额外的工作来执行这样的任务,但是 LINQ 查询表达式可以大大简化这个过程。
假设您希望从数组中仅获取包含嵌入空格的项目,并且希望这些项目按字母顺序列出,您可以构建以下 LINQ 查询表达式:
static void QueryOverStrings()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
// Build a query expression to find the items in the array
// that have an embedded space.
IEnumerable<string> subset =
from g in currentVideoGames
where g.Contains(" ")
orderby g
select g;
// Print out the results.
foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
}
注意,这里创建的查询表达式使用了from
、in
、where
、orderby
和select
LINQ 查询操作符。在本章的后面,你将深入研究查询表达式语法的形式。然而,即使是现在,您也应该能够大致将该语句理解为“给我包含空格的currentVideoGames
中的项目,按字母顺序排列。”
这里,每个匹配搜索标准的项目都被赋予了名称g
(如“game”);然而,任何有效的 C# 变量名都可以。
IEnumerable<string> subset =
from game in currentVideoGames
where game.Contains(" ")
orderby game
select game;
注意,返回的序列保存在一个名为subset
的变量中,该变量的类型是实现通用版本IEnumerable<T>
的类型,其中T
的类型是System.String
(毕竟,您查询的是一个由string
组成的数组)。获得结果集后,您只需使用标准的foreach
结构打印出每一项。如果运行您的应用,您会发现以下输出:
***** Fun with LINQ to Objects *****
Item: Fallout 3
Item: System Shock 2
Item: Uncharted 2
再次使用扩展方法
前面使用的 LINQ 语法(以及本章的其余部分)被称为 LINQ 查询表达式,这是一种类似于 SQL 但略有不同的格式。还有另一种使用扩展方法的语法,这种语法将在本书的大多数示例中使用。
创建一个名为QueryOverStringsWithExtensionMethods()
的新方法,并输入以下代码:
static void QueryOverStringsWithExtensionMethods()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
// Build a query expression to find the items in the array
// that have an embedded space.
IEnumerable<string> subset =
currentVideoGames.Where(g => g.Contains(" ")).OrderBy(g => g).Select(g => g);
// Print out the results.
foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
}
除了用粗体显示的行之外,所有内容都与前面的方法相同。这是使用扩展方法语法。该语法在每个方法中使用 lambda 表达式来定义操作。例如,Where()
方法中的 lambda 定义了条件(其中值包含一个空格)。就像在查询表达式语法中一样,用于表示 lambda 中被求值的值的字母是任意的;我本可以用v
来玩视频游戏。
虽然结果是相同的(运行这个方法产生的输出与前面使用查询表达式的方法相同),但是您很快就会发现结果集的类型略有不同。对于大多数(如果不是几乎所有)场景,这种差异不会导致任何问题,并且这些格式可以互换使用。
又一次,没有 LINQ
可以肯定的是,LINQ 从来不是强制性的。如果您选择这样做,您可以通过完全放弃 LINQ 并使用编程原语(如if
语句和for
循环)来找到相同的结果集。下面是一个方法,它产生与QueryOverStrings()
方法相同的结果,但是方式更加冗长:
static void QueryOverStringsLongHand()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
string[] gamesWithSpaces = new string[5];
for (int i = 0; i < currentVideoGames.Length; i++)
{
if (currentVideoGames[i].Contains(" "))
{
gamesWithSpaces[i] = currentVideoGames[i];
}
}
// Now sort them.
Array.Sort(gamesWithSpaces);
// Print out the results.
foreach (string s in gamesWithSpaces)
{
if( s != null)
{
Console.WriteLine("Item: {0}", s);
}
}
Console.WriteLine();
}
虽然我确信您可以想办法调整前面的方法,但事实是 LINQ 查询可以用来从根本上简化从数据源提取新数据子集的过程。一旦您创建了一个合适的 LINQ 查询,C# 编译器将代表您执行脏活累活,而不是构建嵌套循环、复杂的if
/ else
逻辑、临时数据类型等等。
对 LINQ 结果集的反思
现在,假设Program
类定义了一个名为ReflectOverQueryResults()
的辅助函数,它将打印出 LINQ 结果集的各种细节(注意,该参数是一个System.Object
,用于说明多种类型的结果集)。
static void ReflectOverQueryResults(object resultSet, string queryType = "Query Expressions")
{
Console.WriteLine($"***** Info about your query using {queryType} *****");
Console.WriteLine("resultSet is of type: {0}", resultSet.GetType().Name);
Console.WriteLine("resultSet location: {0}", resultSet.GetType().Assembly.GetName().Name);
}
将QueryOverStrings()
方法的核心更新为:
// Build a query expression to find the items in the array
// that have an embedded space.
IEnumerable<string> subset =
from g in currentVideoGames
where g.Contains(" ")
orderby g
select g;
ReflectOverQueryResults(subset);
// Print out the results.
foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
当您运行应用时,您将会看到subset
变量实际上是通用OrderedEnumerable<TElement, TKey>
类型(表示为OrderedEnumerable
2)的一个实例,它是驻留在
System.Linq.dll`程序集中的一个内部抽象类型。
***** Info about your query using Query Expressions*****
resultSet is of type: OrderedEnumerable`2
resultSet location: System.Linq
对QueryOverStringsWithExtensionMethods()
方法进行相同的更改,除了为第二个参数添加"Extension Methods"
。
// Build a query expression to find the items in the array
// that have an embedded space.
IEnumerable<string> subset =
currentVideoGames
.Where(g => g.Contains(" "))
.OrderBy(g => g)
.Select(g => g);
ReflectOverQueryResults(subset,"Extension Methods");
// Print out the results.
foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
当您运行应用时,您会看到subset
变量是类型SelectIPartitionIterator
的一个实例。如果您从查询中删除Select(g=>g)
,您将回到拥有类型OrderedEnumerable<TElement, TKey>
的实例。这一切意味着什么?对于大多数开发人员来说,并不多(如果有的话)。它们都是从IEnumerable<T>
派生的,都可以用同样的方式迭代,都可以从它们的值创建一个列表或数组。
***** Info about your query using Extension Methods *****
resultSet is of type: SelectIPartitionIterator`2
resultSet location: System.Linq
LINQ 和隐式类型化局部变量
虽然当前的示例程序可以相对容易地确定结果集可以被捕获为string
对象的枚举(例如IEnumerable<string>
),但是我猜想而不是清楚subset
实际上是类型OrderedEnumerable<TElement, TKey>
。
考虑到 LINQ 结果集可以在各种以 LINQ 为中心的名称空间中使用大量类型来表示,定义适当的类型来保存结果集将是乏味的,因为在许多情况下,底层类型可能并不明显,甚至无法从您的代码库直接访问(正如您将看到的,在某些情况下,类型是在编译时生成的)。
为了进一步强调这一点,考虑下面在Program
类中定义的辅助方法:
static void QueryOverInts()
{
int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8};
// Print only items less than 10.
IEnumerable<int> subset = from i in numbers where i < 10 select i;
foreach (int i in subset)
{
Console.WriteLine("Item: {0}", i);
}
ReflectOverQueryResults(subset);
}
在这种情况下,subset
变量是完全不同的底层类型。这一次,实现IEnumerable<int>
接口的类型是一个名为WhereArrayIterator<T>
的低级类。
Item: 1
Item: 2
Item: 3
Item: 8
***** Info about your query *****
resultSet is of type: WhereArrayIterator`1
resultSet location: System.Linq
鉴于 LINQ 查询的确切底层类型肯定不是显而易见的,这些第一个示例将查询结果表示为一个IEnumerable<T>
变量,其中T
是返回序列中的数据类型(string
、int
等)。).然而,这仍然相当麻烦。雪上加霜的是,鉴于IEnumerable<T>
扩展了非泛型IEnumerable
接口,也允许捕获 LINQ 查询的结果,如下所示:
System.Collections.IEnumerable subset =
from i in numbers
where i < 10
select i;
幸运的是,在处理 LINQ 查询时,隐式类型化大大简化了工作。
static void QueryOverInts()
{
int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8};
// Use implicit typing here...
var subset = from i in numbers where i < 10 select i;
// ...and here.
foreach (var i in subset)
{
Console.WriteLine("Item: {0} ", i);
}
ReflectOverQueryResults(subset);
}
根据经验,在捕获 LINQ 查询的结果时,您总是希望利用隐式类型。然而,请记住(在大多数情况下)实数返回值是实现通用IEnumerable<T>
接口的类型。
究竟这种类型是什么在掩盖之下(OrderedEnumerable<TElement, TKey>
、WhereArrayIterator<T>
等)。)无关,没必要发现。如前面的代码示例所示,您可以简单地在一个foreach
构造中使用var
关键字来迭代获取的数据。
LINQ 和扩展方法
尽管当前的例子没有让您直接编写任何扩展方法,但是您实际上是在后台无缝地使用它们。LINQ 查询表达式可用于迭代实现通用IEnumerable<T>
接口的数据容器。然而,System.Array
类类型(用于表示字符串数组和整数数组)并没有而不是实现这个契约。
// The System.Array type does not seem to implement the
// correct infrastructure for query expressions!
public abstract class Array : ICloneable, IList,
IStructuralComparable, IStructuralEquatable
{
...
}
虽然System.Array
没有直接实现IEnumerable<T>
接口,但是它通过静态的System.Linq.Enumerable
类类型间接获得了这种类型(以及许多其他以 LINQ 为中心的成员)所需的功能。
这个实用程序类定义了许多通用的扩展方法(如Aggregate<T>()
、First<T>()
、Max<T>()
等)。),由System.Array
(及其他类型)在后台获取。因此,如果您在currentVideoGames
局部变量上应用点操作符,您会发现在System.Array
的正式定义中有很多成员而不是。
延期执行的作用
关于 LINQ 查询表达式的另一个要点是,当它们返回一个序列时,在对结果序列进行迭代之前,不会对它们进行实际计算。正式来说,这被称为延期执行。这种方法的好处是,您可以对同一个容器多次应用同一个 LINQ 查询,并且可以放心地获得最新和最好的结果。考虑下面对QueryOverInts()
方法的更新:
static void QueryOverInts()
{
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };
// Get numbers less than ten.
var subset = from i in numbers where i < 10 select i;
// LINQ statement evaluated here!
foreach (var i in subset)
{
Console.WriteLine("{0} < 10", i);
}
Console.WriteLine();
// Change some data in the array.
numbers[0] = 4;
// Evaluated again!
foreach (var j in subset)
{
Console.WriteLine("{0} < 10", j);
}
Console.WriteLine();
ReflectOverQueryResults(subset);
}
Note
当 LINQ 语句选择单个元素时(使用First
/ FirstOrDefault
、Single
/ SingleOrDefault
或任何聚合方法),查询会立即执行。下一节将介绍First
、FirstOrDefault
、Single
和SingleOrDefault
。本章稍后将介绍聚合方法。
如果您再次执行该程序,您会发现下面的输出。请注意,第二次迭代请求的序列时,您会发现一个额外的成员,因为您将数组中的第一项设置为小于 10 的值。
1 < 10
2 < 10
3 < 10
8 < 10
4 < 10
1 < 10
2 < 10
3 < 10
8 < 10
Visual Studio 的一个有用的方面是,如果在计算 LINQ 查询之前设置断点,则可以在调试会话期间查看内容。只需将鼠标光标放在 LINQ 结果集变量上(图 13-1 中的subset
)。当您这样做时,您将可以通过展开 Results View 选项来评估查询。
图 13-1
调试 LINQ 表达式
立即执行的作用
当你需要计算一个 LINQ 表达式,产生一个超出foreach
逻辑范围的序列时,你可以调用任意数量的由Enumerable
类型定义的扩展方法,比如ToArray<T>()
、ToDictionary<TSource,TKey>()
和ToList<T>()
。这些方法将导致 LINQ 查询在您调用它们来获取数据快照的同时执行。完成此操作后,可以独立操作数据快照。
此外,如果只查找一个元素,查询会立即执行。First()
返回序列的第一个成员(并且应该总是与orderby
一起使用)。如果没有要返回的内容,例如当原始序列为空或者where
子句过滤掉所有元素时,则FirstOrDefault()
返回列表中项目类型的默认值。Single()
还返回序列的第一个成员(基于orderby
,如果没有orderby
子句,则返回元素顺序)。像它的同名对应物一样,如果序列中没有任何项目(或者所有记录都被where
子句过滤掉)或者所有项目都被where
子句过滤掉,那么SingleOrDefault()
返回元素类型的默认值。First(OrDefault)
和Single(OrDefault)
的区别在于,如果查询将返回多个元素,Single(OrDefault)
将抛出异常。
static void ImmediateExecution()
{
Console.WriteLine();
Console.WriteLine("Immediate Execution");
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };
//get the first element in sequence order
int number = (from i in numbers select i).First();
Console.WriteLine("First is {0}", number);
//get the first in query order
number = (from i in numbers orderby i select i).First();
Console.WriteLine("First is {0}", number);
//get the one element that matches the query
number = (from i in numbers where i > 30 select i).Single();
Console.WriteLine("Single is {0}", number);
try
{
//Throw an exception if more than one element passes the query
number = (from i in numbers where i > 10 select i).Single();
}
catch (Exception ex)
{
Console.WriteLine("An exception occurred: {0}", ex.Message);
}
// Get data RIGHT NOW as int[].
int[] subsetAsIntArray =
(from i in numbers where i < 10 select i).ToArray<int>();
// Get data RIGHT NOW as List<int>.
List<int> subsetAsListOfInts =
(from i in numbers where i < 10 select i).ToList<int>();
}
请注意,整个 LINQ 表达式都被括在括号中,以将其转换为正确的底层类型(无论是什么类型),从而调用Enumerable
的扩展方法。
还记得在第十章中提到的,当 C# 编译器可以明确地确定泛型的类型参数时,你不需要指定类型参数。因此,您也可以如下调用ToArray<T>()
(或ToList<T>()
):
int[] subsetAsIntArray =
(from i in numbers where i < 10 select i).ToArray();
当您需要将 LINQ 查询的结果返回给外部调用者时,立即执行的用处是显而易见的。幸运的是,这恰好是本章的下一个主题。
返回 LINQ 查询的结果
可以在类(或结构)中定义一个字段,其值是 LINQ 查询的结果。但是,要做到这一点,您不能使用隐式类型(因为关键字var
不能用于字段),并且 LINQ 查询的目标不能是实例级数据;因此,它必须是静态的。鉴于这些限制,您很少需要编写如下代码:
class LINQBasedFieldsAreClunky
{
private static string[] currentVideoGames =
{"Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};
// Can't use implicit typing here! Must know type of subset!
private IEnumerable<string> subset =
from g in currentVideoGames
where g.Contains(" ")
orderby g
select g;
public void PrintGames()
{
foreach (var item in subset)
{
Console.WriteLine(item);
}
}
}
通常,LINQ 查询是在方法或属性的范围内定义的。此外,为了简化编程,用于保存结果集的变量将使用关键字var
存储在隐式类型的局部变量中。现在,回想一下第三章中的内容,隐式类型变量不能用来定义参数、返回值或者类或结构的字段。
考虑到这一点,您可能想知道如何将查询结果返回给外部调用者。答案是:看情况。如果你有一个由强类型数据组成的结果集,比如一个字符串数组或者一个Car
的List<T>
,你可以放弃使用var
关键字,使用一个合适的IEnumerable<T>
或者IEnumerable
类型(同样,因为IEnumerable<T>
扩展了IEnumerable
)。考虑以下名为 LinqRetValues 的新控制台应用的示例:
using System;
using System.Collections.Generic;
using System.Linq;
Console.WriteLine("***** LINQ Return Values *****\n");
IEnumerable<string> subset = GetStringSubset();
foreach (string item in subset)
{
Console.WriteLine(item);
}
Console.ReadLine();
static IEnumerable<string> GetStringSubset()
{
string[] colors = {"Light Red", "Green", "Yellow", "Dark Red", "Red", "Purple"};
// Note subset is an IEnumerable<string>-compatible object.
IEnumerable<string> theRedColors = from c in colors where c.Contains("Red") select c;
return theRedColors;
}
结果在意料之中。
Light Red
Dark Red
Red
通过立即执行返回 LINQ 结果
这个例子按预期工作,只是因为这个方法中的返回值和 LINQ 查询是强类型的。如果您使用了var
关键字来定义subset
变量,那么如果该方法仍然原型化为返回IEnumerable<string>
(并且如果隐式类型化的局部变量实际上与指定的返回类型兼容),则只允许返回值*。*
*因为在IEnumerable<T>
上操作有点不方便,可以用立即执行。例如,如果将序列转换为强类型数组,可以简单地返回一个string[]
,而不是返回IEnumerable<string>
。考虑一下Program
类的这个新方法,它做了这样一件事:
static string[] GetStringSubsetAsArray()
{
string[] colors = {"Light Red", "Green", "Yellow", "Dark Red", "Red", "Purple"};
var theRedColors = from c in colors where c.Contains("Red") select c;
// Map results into an array.
return theRedColors.ToArray();
}
有了这个,调用者可以幸福地不知道他们的结果来自于 LINQ 查询,并简单地按照预期使用一组string
s。这里有一个例子:
foreach (string item in GetStringSubsetAsArray())
{
Console.WriteLine(item);
}
当试图将 LINQ 投影的结果返回给调用者时,立即执行也很关键。您将在本章的稍后部分研究这个主题。接下来,让我们看看如何将 LINQ 查询应用于泛型和非泛型集合对象。
将 LINQ 查询应用于集合对象
除了从简单的数据数组中提取结果,LINQ 查询表达式还可以在System.Collections.Generic
名称空间的成员中操作数据,比如List<T>
类型。创建一个名为 LinqOverCollections 的新控制台应用项目,并定义一个基本的Car
类,该类维护当前的速度、颜色、品牌和昵称,如以下代码所示:
namespace LinqOverCollections
{
class Car
{
public string PetName {get; set;} = "";
public string Color {get; set;} = "";
public int Speed {get; set;}
public string Make {get; set;} = "";
}
}
现在,在顶层语句中,定义一个类型为Car
的局部List<T>
变量,并利用对象初始化语法用一些新的Car
对象填充列表。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using LinqOverCollections;
Console.WriteLine("***** LINQ over Generic Collections *****\n");
// Make a List<> of Car objects.
List<Car> myCars = new List<Car>() {
new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"},
new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"},
new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"},
new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo"},
new Car{ PetName = "Melvin", Color = "White", Speed = 43, Make = "Ford"}
};
Console.ReadLine();
访问包含的子对象
将 LINQ 查询应用到通用容器与使用简单数组没有什么不同,因为对象的 LINQ 可以用在任何实现IEnumerable<T>
的类型上。这一次,您的目标是构建一个查询表达式,只选择myCars
列表中的Car
对象,其中速度大于 55。
获得子集后,您将通过调用PetName
属性打印出每个Car
对象的名称。假设您有下面的 helper 方法(带一个List<Car>
参数),它是从顶层语句调用的:
static void GetFastCars(List<Car> myCars)
{
// Find all Car objects in the List<>, where the Speed is
// greater than 55.
var fastCars = from c in myCars where c.Speed > 55 select c;
foreach (var car in fastCars)
{
Console.WriteLine("{0} is going too fast!", car.PetName);
}
}
请注意,您的查询表达式只从List<T>
中获取那些属性大于 55 的项目。如果您运行该应用,您会发现只有Henry
和Daisy
两项符合搜索条件。
如果您想要构建一个更复杂的查询,您可能想要只查找那些Speed
值大于 90 的 BMW。为此,只需使用 C# &&
操作符构建一个复合布尔语句。
static void GetFastBMWs(List<Car> myCars)
{
// Find the fast BMWs!
var fastCars = from c in myCars where c.Speed > 90 && c.Make == "BMW" select c;
foreach (var car in fastCars)
{
Console.WriteLine("{0} is going too fast!", car.PetName);
}
}
这种情况下,唯一打印出来的宠物名是Henry
。
将 LINQ 查询应用于非泛型集合
回想一下,LINQ 的查询操作符被设计成可以处理任何实现IEnumerable<T>
的类型(直接或者通过扩展方法)。鉴于System.Array
已经提供了这种必要的基础设施,您可能会惊讶于System.Collections
中的遗留(非通用)容器还没有。幸运的是,仍然可以使用泛型Enumerable.OfType<T>()
扩展方法迭代非泛型集合中包含的数据。
当从非通用集合对象(如ArrayList
)调用OfType<T>()
时,只需在容器中指定项目的类型,以提取兼容的IEnumerable<T>
对象。在代码中,可以使用隐式类型的变量存储该数据点。
考虑下面的新方法,它用一组Car
对象填充ArrayList
(确保将System.Collections
名称空间导入到您的Program.cs
文件中):
static void LINQOverArrayList()
{
Console.WriteLine("***** LINQ over ArrayList *****");
// Here is a nongeneric collection of cars.
ArrayList myCars = new ArrayList() {
new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"},
new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"},
new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"},
new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo"},
new Car{ PetName = "Melvin", Color = "White", Speed = 43, Make = "Ford"}
};
// Transform ArrayList into an IEnumerable<T>-compatible type.
var myCarsEnum = myCars.OfType<Car>();
// Create a query expression targeting the compatible type.
var fastCars = from c in myCarsEnum where c.Speed > 55 select c;
foreach (var car in fastCars)
{
Console.WriteLine("{0} is going too fast!", car.PetName);
}
}
和前面的例子一样,当从顶层语句调用这个方法时,它将根据 LINQ 查询的格式只显示名字Henry
和Daisy
。
使用 OfType ()过滤数据
如你所知,非泛型类型可以包含任何项目的组合,因为这些容器的成员(比如ArrayList
)被原型化为接收System.Object
。例如,假设一个ArrayList
包含各种项目,其中只有一个子集是数字的。如果您想获得一个只包含数字数据的子集,您可以使用OfType<T>()
来实现,因为它会在迭代过程中过滤掉类型不同于给定类型的每个元素。
static void OfTypeAsFilter()
{
// Extract the ints from the ArrayList.
ArrayList myStuff = new ArrayList();
myStuff.AddRange(new object[] { 10, 400, 8, false, new Car(), "string data" });
var myInts = myStuff.OfType<int>();
// Prints out 10, 400, and 8.
foreach (int i in myInts)
{
Console.WriteLine("Int value: {0}", i);
}
}
至此,您已经有机会将 LINQ 查询应用于数组、泛型集合和非泛型集合。这些容器保存了 C# 基本类型(整数、字符串数据)以及自定义类。下一个任务是学习更多的 LINQ 操作符,这些操作符可以用来构建更复杂、更有用的查询。
调查 C# LINQ 查询运算符
C# 定义了大量现成的查询操作符。表 13-2 记录了一些更常用的查询运算符。除了表 13-2 中显示的部分操作符列表外,System.Linq.Enumerable
类还提供了一组方法,这些方法没有直接的 C# 查询操作符简写符号,而是作为扩展方法公开。可以调用这些通用方法以各种方式转换结果集(Reverse<>()
、ToArray<>()
、ToList<>()
等)。).一些用于从结果集中提取单例,另一些执行各种集合操作(Distinct<>()
、Union<>()
、Intersect<>()
等)。),还有一些汇总结果(Count<>()
、Sum<>()
、Min<>()
、Max<>()
等)。).
表 13-2。
常见的 LINQ 查询运算符
|查询运算符
|
生命的意义
|
| — | — |
| from
,in
| 用于定义任何 LINQ 表达式的主干,这允许您从拟合容器中提取数据的子集。 |
| where
| 用于定义从容器中提取哪些项目的限制。 |
| select
| 用于从容器中选择一个序列。 |
| join
、on
、equals
、into
| 基于指定的键执行联接。请记住,这些“连接”不需要与关系数据库中的数据有任何关系。 |
| orderby
、ascending
、descending
| 允许结果子集按升序或降序排序。 |
| groupby
| 生成包含按指定值分组的数据的子集。 |
要开始研究更复杂的 LINQ 查询,请创建一个名为 FunWithLinqExpressions 的新控制台应用项目。接下来,您需要定义一些样本数据的数组或集合。对于这个项目,您将创建一个由以下代码定义的ProductInfo
对象组成的数组:
namespace FunWithLinqExpressions
{
class ProductInfo
{
public string Name {get; set;} = "";
public string Description {get; set;} = "";
public int NumberInStock {get; set;} = 0;
public override string ToString()
=> $"Name={Name}, Description={Description}, Number in Stock={NumberInStock}";
}
}
现在用调用代码中的一批ProductInfo
对象填充一个数组。
Console.WriteLine("***** Fun with Query Expressions *****\n");
// This array will be the basis of our testing...
ProductInfo[] itemsInStock = new[] {
new ProductInfo{ Name = "Mac's Coffee", Description = "Coffee with TEETH", NumberInStock = 24},
new ProductInfo{ Name = "Milk Maid Milk", Description = "Milk cow's love", NumberInStock = 100},
new ProductInfo{ Name = "Pure Silk Tofu", Description = "Bland as Possible", NumberInStock = 120},
new ProductInfo{ Name = "Crunchy Pops", Description = "Cheezy, peppery goodness", NumberInStock = 2},
new ProductInfo{ Name = "RipOff Water", Description = "From the tap to your wallet", NumberInStock = 100},
new ProductInfo{ Name = "Classic Valpo Pizza", Description = "Everyone loves pizza!", NumberInStock = 73}
};
// We will call various methods here!
Console.ReadLine();
基本选择语法
因为 LINQ 查询表达式的语法正确性是在编译时验证的,所以您需要记住这些运算符的顺序非常重要。用最简单的话来说,每个 LINQ 查询表达式都是使用from
、in
和select
操作符构建的。以下是要遵循的通用模板:
var result =
from matchingItem in container
select matchingItem;
from
操作符后面的项表示与 LINQ 查询条件匹配的项,它可以被命名为您选择的任何名称。in
操作符后面的项表示要搜索的数据容器(数组、集合、XML 文档等。).
下面是一个简单的查询,只需选择容器中的每一项(行为类似于数据库Select *
SQL 语句)。请考虑以下几点:
static void SelectEverything(ProductInfo[] products)
{
// Get everything!
Console.WriteLine("All product details:");
var allProducts = from p in products select p;
foreach (var prod in allProducts)
{
Console.WriteLine(prod.ToString());
}
}
老实说,这个查询表达式并不完全有用,因为您的子集与传入参数中的数据子集相同。如果您愿意,可以使用以下选择语法只提取每辆汽车的Name
值:
static void ListProductNames(ProductInfo[] products)
{
// Now get only the names of the products.
Console.WriteLine("Only product names:");
var names = from p in products select p.Name;
foreach (var n in names)
{
Console.WriteLine("Name: {0}", n);
}
}
获取数据子集
要从容器中获取特定的子集,可以使用where
操作符。执行此操作时,通用模板现在变成以下代码:
var result =
from item
in container
where BooleanExpression
select item;
注意,where
操作符期望一个解析为布尔值的表达式。例如,要从ProductInfo[]
参数中仅提取手头有超过 25 个项目的项目,您可以编写以下代码:
static void GetOverstock(ProductInfo[] products)
{
Console.WriteLine("The overstock items!");
// Get only the items where we have more than
// 25 in stock.
var overstock =
from p
in products
where p.NumberInStock > 25
select p;
foreach (ProductInfo c in overstock)
{
Console.WriteLine(c.ToString());
}
}
如本章前面所示,当您构建一个where
子句时,允许使用任何有效的 C# 操作符来构建复杂的表达式。例如,回想一下这个查询,它只提取时速至少为 100 英里的宝马:
// Get BMWs going at least 100 MPH.
var onlyFastBMWs =
from c
in myCars
where c.Make == "BMW" && c.Speed >= 100
select c;
投影新的数据类型
也可以从现有的数据源投射新形式的数据。让我们假设您想要接受传入的ProductInfo[]
参数,并获得一个只包含每一项的名称和描述的结果集。为此,您可以定义一个select
语句来动态生成一个新的匿名类型。
static void GetNamesAndDescriptions(ProductInfo[] products)
{
Console.WriteLine("Names and Descriptions:");
var nameDesc =
from p
in products
select new { p.Name, p.Description };
foreach (var item in nameDesc)
{
// Could also use Name and Description properties
// directly.
Console.WriteLine(item.ToString());
}
}
请记住,当您的 LINQ 查询使用投影时,您无法知道底层的数据类型,因为这是在编译时确定的。在这些情况下,var
关键字是必需的。同样,回想一下,您不能创建具有隐式类型返回值的方法。因此,下面的方法不会编译:
static var GetProjectedSubset(ProductInfo[] products)
{
var nameDesc =
from p in products select new { p.Name, p.Description };
return nameDesc; // Nope!
}
当您需要将投影数据返回给调用者时,一种方法是使用ToArray()
扩展方法将查询结果转换成System.Array
对象。因此,如果您要按如下方式更新查询表达式:
// Return value is now an Array.
static Array GetProjectedSubset(ProductInfo[] products)
{
var nameDesc =
from p in products select new { p.Name, p.Description };
// Map set of anonymous objects to an Array object.
return nameDesc.ToArray();
}
您可以调用并处理数据,如下所示:
Array objs = GetProjectedSubset(itemsInStock);
foreach (object o in objs)
{
Console.WriteLine(o); // Calls ToString() on each anonymous object.
}
请注意,您必须使用文字System.Array
对象,并且不能使用 C# 数组声明语法,因为您不知道底层类型,因为您正在对编译器生成的匿名类进行操作!还要注意,您没有为泛型ToArray<T>()
方法指定类型参数,因为您在编译时才知道底层的数据类型,这对于您的目的来说已经太晚了。
明显的问题是您丢失了任何强类型,因为Array
对象中的每一项都被假定为类型Object
。然而,当您需要返回一个 LINQ 结果集,它是一个匿名类型的投影操作的结果时,将数据转换成一个Array
类型(或者通过Enumerable
类型的其他成员转换成另一个合适的容器)是强制性的。
投影到不同的数据类型
除了投射到匿名类型,您还可以将 LINQ 查询的结果投射到另一个具体类型。这允许静态输入并使用IEnumerable<T>
作为结果集。首先,创建一个较小版本的ProductInfo
类。
namespace FunWithLinqExpressions
{
class ProductInfoSmall
{
public string Name {get; set;} = "";
public string Description {get; set;} = "";
public override string ToString()
=> $"Name={Name}, Description={Description}";
}
}
下一个变化是将查询结果投射到一组ProductInfoSmall
对象中,而不是匿名类型。将以下方法添加到您的类中:
static void GetNamesAndDescriptionsTyped(
ProductInfo[] products)
{
Console.WriteLine("Names and Descriptions:");
IEnumerable<ProductInfoSmall> nameDesc =
from p
in products
select new ProductInfoSmall
{ Name=p.Name, Description=p.Description };
foreach (ProductInfoSmall item in nameDesc)
{
Console.WriteLine(item.ToString());
}
}
使用 LINQ 投影,您可以选择使用哪种方法(匿名或强类型对象)。您做出的决定完全取决于您的业务需求。
使用可枚举获得计数
当您计划新的数据批次时,您可能需要准确地发现有多少项被返回到序列中。任何时候,当您需要确定从 LINQ 查询表达式返回的项数时,只需使用Enumerable
类的Count()
扩展方法。例如,下面的方法将在一个本地数组中查找所有长度超过六个字符的string
对象:
static void GetCountFromQuery()
{
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
// Get count from the query.
int numb = (from g in currentVideoGames where g.Length > 6 select g).Count();
// Print out the number of items.
Console.WriteLine("{0} items honor the LINQ query.", numb);
}
反转结果集
使用Enumerable
类的Reverse<>()
扩展方法,可以非常简单地反转结果集中的项目。例如,以下方法从传入的ProductInfo[]
参数中反向选择所有项目:
static void ReverseEverything(ProductInfo[] products)
{
Console.WriteLine("Product in reverse:");
var allProducts = from p in products select p;
foreach (var prod in allProducts.Reverse())
{
Console.WriteLine(prod.ToString());
}
}
排序表达式
正如你在本章最初的例子中所看到的,查询表达式可以使用一个orderby
操作符来按照特定的值对子集中的项目进行排序。默认情况下,顺序将是升序;因此,按字符串排序将是字母顺序,按数字数据排序将是从低到高,依此类推。如果您需要以降序查看结果,只需包含descending
操作符。思考以下方法:
static void AlphabetizeProductNames(ProductInfo[] products)
{
// Get names of products, alphabetized.
var subset = from p in products orderby p.Name select p;
Console.WriteLine("Ordered by Name:");
foreach (var p in subset)
{
Console.WriteLine(p.ToString());
}
}
虽然升序是默认的,但是您可以使用ascending
操作符来表达您的意图。
var subset = from p in products orderby p.Name ascending select p;
如果您想按降序排列项目,您可以通过descending
操作符来实现。
var subset = from p in products orderby p.Name descending select p;
LINQ 作为一个更好的文氏作图工具
Enumerable
类支持一组扩展方法,允许您使用两个(或更多)LINQ 查询作为基础来查找数据的联合、差异、连接和交集。首先,考虑一下Except()
扩展方法,它将返回一个 LINQ 结果集,其中包含两个容器之间的差异,在本例中是值Yugo
。
static void DisplayDiff()
{
List<string> myCars =
new List<String> {"Yugo", "Aztec", "BMW"};
List<string> yourCars =
new List<String>{"BMW", "Saab", "Aztec" };
var carDiff =
(from c in myCars select c)
.Except(from c2 in yourCars select c2);
Console.WriteLine("Here is what you don't have, but I do:");
foreach (string s in carDiff)
{
Console.WriteLine(s); // Prints Yugo.
}
}
Intersect()
方法将返回一个结果集,该结果集包含一组容器中的公共数据项。例如,以下方法返回序列Aztec
和BMW
:
static void DisplayIntersection()
{
List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" };
List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" };
// Get the common members.
var carIntersect =
(from c in myCars select c)
.Intersect(from c2 in yourCars select c2);
Console.WriteLine("Here is what we have in common:");
foreach (string s in carIntersect)
{
Console.WriteLine(s); // Prints Aztec and BMW.
}
}
正如您所猜测的,Union()
方法返回一个包含一批 LINQ 查询的所有成员的结果集。像任何适当的联合一样,如果一个公共成员出现多次,您将不会发现重复值。因此,下面的方法将打印出值Yugo
、Aztec
、BMW
和Saab
:
static void DisplayUnion()
{
List<string> myCars =
new List<string> { "Yugo", "Aztec", "BMW" };
List<string> yourCars =
new List<String> { "BMW", "Saab", "Aztec" };
// Get the union of these containers.
var carUnion =
(from c in myCars select c)
.Union(from c2 in yourCars select c2);
Console.WriteLine("Here is everything:");
foreach (string s in carUnion)
{
Console.WriteLine(s); // Prints all common members.
}
}
最后,Concat()
扩展方法返回一个结果集,它是 LINQ 结果集的直接串联。例如,下面的方法打印出结果Yugo
、Aztec
、BMW
、BMW
、Saab
和Aztec
:
static void DisplayConcat()
{
List<string> myCars =
new List<String> { "Yugo", "Aztec", "BMW" };
List<string> yourCars =
new List<String> { "BMW", "Saab", "Aztec" };
var carConcat =
(from c in myCars select c)
.Concat(from c2 in yourCars select c2);
// Prints:
// Yugo Aztec BMW BMW Saab Aztec.
foreach (string s in carConcat)
{
Console.WriteLine(s);
}
}
删除重复项
当您调用Concat()
扩展方法时,您很可能在获取的结果中得到冗余条目,这在某些情况下可能正是您想要的。但是,在其他情况下,您可能希望删除数据中的重复条目。为此,只需调用Distinct()
扩展方法,如下所示:
static void DisplayConcatNoDups()
{
List<string> myCars =
new List<String> { "Yugo", "Aztec", "BMW" };
List<string> yourCars =
new List<String> { "BMW", "Saab", "Aztec" };
var carConcat =
(from c in myCars select c)
.Concat(from c2 in yourCars select c2);
// Prints:
// Yugo Aztec BMW Saab.
foreach (string s in carConcat.Distinct())
{
Console.WriteLine(s);
}
}
LINQ 聚合运算
LINQ 查询还可以设计为对结果集执行各种聚合操作。Count()
扩展方法就是这样一个聚合例子。其他可能性包括使用Enumerable
类的Max()
、Min()
、Average()
或Sum()
成员获得平均值、最大值、最小值或值的总和。这里有一个简单的例子:
static void AggregateOps()
{
double[] winterTemps = { 2.0, -21.3, 8, -4, 0, 8.2 };
// Various aggregation examples.
Console.WriteLine("Max temp: {0}",
(from t in winterTemps select t).Max());
Console.WriteLine("Min temp: {0}",
(from t in winterTemps select t).Min());
Console.WriteLine("Average temp: {0}",
(from t in winterTemps select t).Average());
Console.WriteLine("Sum of all temps: {0}",
(from t in winterTemps select t).Sum());
}
这些例子应该给你足够的知识,让你对构建 LINQ 查询表达式的过程感到舒服。虽然您还没有研究其他操作符,但是当您学习相关的 LINQ 技术时,将会在本文后面看到更多的例子。为了总结你对 LINQ 的初步了解,本章的剩余部分将深入到 C# LINQ 查询操作符和底层对象模型之间的细节。
LINQ 查询语句的内部表示
至此,您已经了解了使用各种 C# 查询操作符(如from
、in
、where
、orderby
和select
)构建查询表达式的过程。此外,您发现 LINQ 到对象 API 的一些功能只有在调用Enumerable
类的扩展方法时才能被访问。然而,事实是,当编译 LINQ 查询时,C# 编译器将所有 C# LINQ 操作符翻译成对Enumerable
类方法的调用。
大量的Enumerable
方法已经被原型化,将委托作为参数。许多方法需要一个名为Func<>
的泛型委托,这是在第十章的泛型委托研究中介绍的。考虑一下Enumerable
的Where()
方法,当您使用 C# where
LINQ 查询操作符时,它会以您的名义被调用。
// Overloaded versions of the Enumerable.Where<T>() method.
// Note the second parameter is of type System.Func<>.
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
System.Func<TSource,int,bool> predicate)
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
System.Func<TSource,bool> predicate)
Func<>
委托(顾名思义)用一组多达 16 个参数和一个返回值表示给定函数的模式。如果您使用 Visual Studio 对象浏览器来检查这种类型,您会注意到各种形式的Func<>
委托。这里有一个例子:
// The various formats of the Func<> delegate.
public delegate TResult Func<T1,T2,T3,T4,TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
public delegate TResult Func<T1,T2,T3,TResult>(T1 arg1, T2 arg2, T3 arg3)
public delegate TResult Func<T1,T2,TResult>(T1 arg1, T2 arg2)
public delegate TResult Func<T1,TResult>(T1 arg1)
public delegate TResult Func<TResult>()
鉴于System.Linq.Enumerable
的许多成员需要一个委托作为输入,当调用它们时,您可以手动创建一个新的委托类型并编写必要的目标方法,使用 C# 匿名方法,或者定义一个适当的 lambda 表达式。不管你采取哪种方法,结果都是一样的。
虽然使用 C# LINQ 查询操作符确实是构建 LINQ 查询表达式最简单的方法,但是让我们来看看这些可能的方法,这样您就可以看到 C# 查询操作符和底层的Enumerable
类型之间的联系。
用查询运算符构建查询表达式(重访)
首先,创建一个名为 LinqUsingEnumerable 的新控制台应用项目。Program
类将定义一系列静态帮助器方法(每个方法都在顶级语句中调用),以说明构建 LINQ 查询表达式的各种方式。
第一种方法QueryStringsWithOperators()
提供了构建查询表达式的最直接的方法,与本章前面的 LinqOverArray 示例中显示的代码相同。
using System.Linq;
static void QueryStringWithOperators()
{
Console.WriteLine("***** Using Query Operators *****");
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
var subset = from game in currentVideoGames
where game.Contains(" ") orderby game select game;
foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
}
使用 C# 查询操作符构建查询表达式的明显好处是,Func<>
委托和对Enumerable
类型的调用是看不见也想不到的,因为执行这种翻译是 C# 编译器的工作。当然,使用各种查询操作符(from
、in
、where
或orderby
)构建 LINQ 表达式是最常见和最直接的方法。
使用可枚举类型和 Lambda 表达式构建查询表达式
请记住,这里使用的 LINQ 查询操作符只是调用由Enumerable
类型定义的各种扩展方法的简写版本。考虑下面的QueryStringsWithEnumerableAndLambdas()
方法,它现在直接使用Enumerable
扩展方法处理本地字符串数组:
static void QueryStringsWithEnumerableAndLambdas()
{
Console.WriteLine("***** Using Enumerable / Lambda Expressions *****");
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
// Build a query expression using extension methods
// granted to the Array via the Enumerable type.
var subset = currentVideoGames
.Where(game => game.Contains(" "))
.OrderBy(game => game).Select(game => game);
// Print out the results.
foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}
在这里,首先调用currentVideoGames
字符串数组上的Where()
扩展方法。回想一下,Array
类通过Enumerable
授予的扩展方法接收这个。Enumerable.Where()
方法需要一个System.Func<T1, TResult>
委托参数。该委托的第一个类型参数表示要处理的与IEnumerable<T>
兼容的数据(在本例中是一个字符串数组),而第二个类型参数表示方法结果数据,该数据是从 lambda 表达式中的一个语句获得的。
在这个代码示例中,Where()
方法的返回值是隐藏的,但是在幕后,您操作的是一个OrderedEnumerable
类型。从这个对象中,您调用通用的OrderBy()
方法,它也需要一个Func<>
委托参数。这一次,您只是通过一个合适的 lambda 表达式依次传递每一项。调用OrderBy()
的结果是初始数据的一个新的有序序列。
最后,您调用从OrderBy()
返回的序列的Select()
方法,这导致最终的数据集存储在名为subset
的隐式类型变量中。
可以肯定的是,这个“手写的”LINQ 查询比前面的 C# LINQ 查询操作符示例要复杂得多。毫无疑问,部分复杂性是由于使用点运算符将调用链接在一起。下面是同一个查询,其中每一步都被分解成离散的块(正如您可能猜到的,您可以用各种方式分解整个查询):
static void QueryStringsWithEnumerableAndLambdas2()
{
Console.WriteLine("***** Using Enumerable / Lambda Expressions *****");
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
// Break it down!
var gamesWithSpaces = currentVideoGames.Where(game => game.Contains(" "));
var orderedGames = gamesWithSpaces.OrderBy(game => game);
var subset = orderedGames.Select(game => game);
foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}
您可能同意,直接使用Enumerable
类的方法构建 LINQ 查询表达式比使用 C# 查询操作符要冗长得多。同样,考虑到Enumerable
的方法需要委托作为参数,您通常需要编写 lambda 表达式,以允许底层委托目标处理输入数据。
使用可枚举类型和匿名方法构建查询表达式
假设 C# lambda 表达式只是使用匿名方法的简写符号,考虑在QueryStringsWithAnonymousMethods()
helper 函数中创建的第三个查询表达式,如下所示:
static void QueryStringsWithAnonymousMethods()
{
Console.WriteLine("***** Using Anonymous Methods *****");
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
// Build the necessary Func<> delegates using anonymous methods.
Func<string, bool> searchFilter = delegate(string game) { return game.Contains(" "); };
Func<string, string> itemToProcess = delegate(string s) { return s; };
// Pass the delegates into the methods of Enumerable.
var subset = currentVideoGames.Where(searchFilter).OrderBy(itemToProcess).Select(itemToProcess);
// Print out the results.
foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}
这个查询表达式的迭代更加冗长,因为您正在手动创建由Enumerable
类的Where()
、OrderBy()
和Select()
方法使用的Func<>
委托。有利的一面是,匿名方法语法确实将所有的委托处理包含在一个方法定义中。然而,该方法在功能上等同于前面章节中创建的QueryStringsWithEnumerableAndLambdas()
和QueryStringsWithOperators()
方法。
使用可枚举类型和原始委托构建查询表达式
最后,如果您想使用详细方法构建一个查询表达式,您可以避免使用 lambdas/anonymous 方法语法,直接为每个Func<>
类型创建委托目标。这是查询表达式的最后一次迭代,在名为VeryComplexQueryExpression
的新类类型中建模:
class VeryComplexQueryExpression
{
public static void QueryStringsWithRawDelegates()
{
Console.WriteLine("***** Using Raw Delegates *****");
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
// Build the necessary Func<> delegates.
Func<string, bool> searchFilter =
new Func<string, bool>(Filter);
Func<string, string> itemToProcess =
new Func<string,string>(ProcessItem);
// Pass the delegates into the methods of Enumerable.
var subset =
currentVideoGames
.Where(searchFilter)
.OrderBy(itemToProcess)
.Select(itemToProcess);
// Print out the results.
foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}
// Delegate targets.
public static bool Filter(string game)
{
return game.Contains(" ");
}
public static string ProcessItem(string game)
{
return game;
}
}
您可以通过在Program
类的顶级语句中调用该方法来测试字符串处理逻辑的迭代,如下所示:
VeryComplexQueryExpression.QueryStringsWithRawDelegates();
如果您现在运行应用来测试每种可能的方法,那么无论采用哪种方法,输出都是相同的就不足为奇了。关于 LINQ 查询表达式如何在幕后表示,请记住以下几点:
-
查询表达式是使用各种 C# 查询运算符创建的。
-
查询操作符只是调用由
System.Linq.Enumerable
类型定义的扩展方法的简写符号。 -
Enumerable
的许多方法需要委托(特别是Func<>
)作为参数。 -
任何需要委托参数的方法都可以被传递一个 lambda 表达式。
-
Lambda 表达式只是伪装的匿名方法(大大提高了可读性)。
-
匿名方法是分配原始委托和手动构建委托目标方法的简写符号。
咻!这可能比你想的要深入一些,但是我希望这个讨论能够帮助你理解用户友好的 C# 查询操作符在幕后做了什么。
摘要
LINQ 是一组相关的技术,试图提供一种单一的、对称的方式来与不同形式的数据进行交互。正如本章所解释的,LINQ 可以与任何实现IEnumerable<T>
接口的类型交互,包括简单的数组以及通用和非通用的数据集合。
正如您所看到的,使用 LINQ 技术是通过几个 C# 语言特性来完成的。例如,假设 LINQ 查询表达式可以返回任意数量的结果集,那么通常使用var
关键字来表示底层数据类型。此外,lambda 表达式、对象初始化语法和匿名类型都可以用来构建功能性和紧凑的 LINQ 查询。
更重要的是,您已经看到了 C# LINQ 查询操作符是如何简单地对System.Linq.Enumerable
类型的静态成员进行调用的简写符号。如图所示,Enumerable
的大多数成员操作的是Func<T>
委托类型,它可以将文字方法地址、匿名方法或 lambda 表达式作为输入来评估查询。**