LINQ 内部






4.14/5 (15投票s)
本文介绍了 LINQ 及其他相关的语言扩展。
引言
今天的编程语言支持各种存储结构,用于永久或临时存储数据,例如关系数据库、XML、集合和数组。这些不同的存储结构暴露了不同的 API 来操作数据;例如,与关系数据源交互时,我们使用 ADO/SQL 接口,而对于 XML 数据存储,我们使用 XML/XPath 库。其中一些数据存储选项暴露了非常强大的数据操作 API,如 SQL 和 XPath,而另一些存储选项提供了非常简单的接口,如集合和数组。确实,所有这些存储选项都非常强大,但这些数据存储选项的使用与通用编程语言之间仍然存在差距。
LINQ(作为 Microsoft 长期研究项目的结果)提供了一个统一而强大的接口,用于跨不同数据源操作数据。它暴露了一种类似于 SQL 的查询语言,即使数据存储在数组、集合、XML 或数据集中,也能操作数据。您还会欣赏到,与其他数据存储接口(如 XPath/SQL)不同,LINQ 提供了一个强类型接口。因此,所有强类型编译器(如 C#)都可以确保使用 LINQ 的应用程序是类型安全的。
LINQ 被设计为一种可扩展技术,当前版本的目标是关系数据库、数据集、集合、数组和实现 IEnumerable
接口的对象。除了 LINQ 之外,Microsoft 还引入了一些语言扩展,使 LINQ 更强大且易于使用。因此,在我们真正开始学习 LINQ 之前,让我们先概述一下这些语言扩展。
隐式类型变量和数组
在 Microsoft .NET 平台中,所有变量在使用之前都需要定义并具有类型。C# 3.0 允许您定义隐式类型变量,其中变量的类型由编译器推断。
public void LetsDeclareSomeImplicitlyTypedVariables()
{
var myVar1 = 0;
var myVar2 = true;
var myVar3 = "Lazy fox jumps over the brown gate";
}
隐式类型变量使用关键字 var
定义。严格来说,var
并不像 Visual Basic 中的变体变量那样表示变体变量。此关键字只是请求编译器根据初始值推断变量的类型。在上面的示例中,myVar1
被推断为整数变量,myVar2
被推断为布尔值,myVar3
被推断为字符串变量。在 CLR 中,没有名为 var
的东西,因此如果您使用隐式类型变量反汇编代码,您会发现 C# 编译器在编译期间为所有隐式类型变量定义了适当的类型。上面示例的反汇编版本如下:
[通过 Reflector 反汇编]
public string LetsDeclareSomeImplicitlyTypedVariables()
{
int myVar1 = 0;
bool myVar2 = true;
string myVar3 = "Lazy fox jumps over the brown gate";
}
只要编译器能够在编译期间确定变量的类型,您就可以为任何类型定义隐式类型变量。以下所有都是有效的隐式类型变量:
var myIntegerArray =
new int[] { 1, 2, 3, 4, 6, 6, 7, 8 };
|
编译器转换为 |
var myGenericListOfTypeMyClass =
new List<MyClass>();
|
编译器转换为 |
var myClass = new MyClass();
|
编译器转换为 |
int[] myIntegerArray =
new int[] { 1, 2, 3, 4, 6, 6, 7, 8 };
foreach (var item in myIntegerArray )
{
Console.WriteLine("Item value: {0}", item);
}
|
编译器转换为 int[] myIntegerArray =
new int[] { 1, 2, 3, 4, 6, 6, 7, 8 };
foreach (int item in myIntegerArray )
{
Console.WriteLine("Item value: {0}", item);
}
|
与隐式类型变量类似,C# 3.0 允许您定义隐式类型数组,如下所示:
var myArray = new[] { 1, 10, 100, 1000 };
以上语句将被编译为 int[] myArray = new int[] { 1, 10, 100, 1000 };
。
与隐式类型变量相关的一些限制如下:
- 隐式类型变量只能在方法内部声明为局部变量,并且必须用一些初始值进行初始化。
- 定义可空隐式类型局部变量是非法的。
- 使用
var
关键字定义返回类型、参数或类型的字段数据是非法的。
匿名类型
在 C# 的早期版本中,您需要声明一个结构或类来封装数据。然而,您会同意在某些情况下,您希望在本地封装数据,而不带任何关联的方法或事件。对于这种情况,定义类或结构可能相当繁琐和耗时。C# 3.0 引入了一个名为“匿名类型”的功能,它允许您在没有正确定义类或结构的情况下封装数据。以下示例创建了一个匿名类型:
public string LetsDeclareSomeAnonymousTyoe()
{
var BoxAnonymousType = new { Color = "Blue", Weight = 24.0, Height = 45, Width=45.34 };
}
在上面的示例中,我们创建了一个具有这些成员的匿名类型:Color
、Weight
、Height
和 Width
。与隐式类型变量类似,CLR 没有任何匿名类型的概念。在编译期间,C# 编译器声明一个具有唯一名称的类,并具有四个 readonly
属性 Color
、Blue
、Height
和 Width
。隐式类型变量 BoxAnonymousType
使用编译器生成的类进行初始化。如果您通过 ILDASM 或 Reflector 反汇编上面的代码,您将看到编译器生成的类。
所有匿名类型的编译器生成类都派生自 System.Object
并覆盖了 Equal
、GetHashCode
和 ToString
方法。匿名类型的编译器生成类的 ToString
实现只是从每个名称/值对构建一个字符串。因此,BoxAnonymousType.ToString()
将返回 {Color = "Blue", Weight = "24.0", Height = "45", Width="45.34"}
。GetHashCode
通过使用每个名称/值对来计算哈希码,因此,如果编译器生成类的两个对象对每个名称/值对都具有完全相同的值,则 GetHashCode
方法将返回相同的值。Equal
方法比较每个名称/值对的值,因此如果编译器生成类的两个对象具有完全相同的值,则返回 true。请注意,==
运算符(作为默认行为)仍将比较编译器生成类的引用,而不是值。通过匿名类型中这些方法的默认实现,匿名类型非常适合包含在哈希表中。
匿名类型另一个重要方面是,编译器会为所有相似的匿名类型(即具有相同属性、名称和类型)生成一个类。
扩展方法
一旦类型被编译,它的定义就或多或少是最终的。添加新功能或成员的唯一方法是重新编写代码并重新编译。因此,如果您无法访问源代码,就无法在已编译的类型中添加成员。假设您想在 System.Object
类中添加一个方法,该方法返回对象的程序集名称。由于所有类型都直接或间接继承自 System.Object
,因此在 System.Object
中添加方法意味着在所有类型中添加方法。由于您无法访问 System.Object
的代码库,因此在 System.Object
中添加此功能似乎非常不可能。C# 3.0 引入了一个名为“扩展方法”的新功能,允许您在已编译的类型中添加新方法。以下示例使用扩展方法在 System.Object
类中添加新方法。
public static class MyExtensionClass
{
public static string AssemblyName(this System.Object obj)
{
return obj.GetType().Assembly.FullName;
}
}
所有扩展方法都需要在静态类中定义,即所有扩展方法都必须定义为 static
。上面的示例创建了一个名为 AssemblyName
的扩展方法。参数 this
表示它是一个扩展方法。下一个参数表示此扩展方法所属的类型,即 System.Object
。由于此方法最初不是 System.Object
的一部分,我们需要声明一个变量来保存对象的引用。在上面的示例中,obj
持有引用。通过此引用,扩展方法可以访问对象的属性和方法。
一旦定义了扩展方法,您就可以像访问其他 System.Object
成员一样访问它,并且它在 Visual Studio 的成员列表中也是可见的。
[注意:AssemblyName 旁边的向下箭头表示它是一个扩展方法]
扩展方法也可以接受参数。以下扩展方法被添加到类型 int
中,用于将 int
与给定值进行比较。
public static class MyExtensionClass
{
public static bool iSGreater(this int currentInt, int value)
{
return currentInt > value;
}
}
Int myInt = 450;
Bool b = myInt.iSGreater (350);
与 C# 3.0 的其他两个功能一样,扩展方法也是一种语言扩展,在 CLR 中没有任何意义。在编译期间,C# 将扩展方法调用替换为静态方法的常规调用。因此,myInt.iSGreater (350)
将被替换为 MyExtensionClass.iSGreater(myInt, 350)
。因此,C# 编译器和 Visual Studio 共同让开发人员感觉 AssemblyName
是在 System.Object
类中定义的。
LINQ 广泛使用扩展方法,因此,在深入了解 LINQ 之前,让我们构建一个扩展方法库,这将有助于我们理解 LINQ。以下是我们构建库将使用的模板:
namespace MyLibrary
{
public static class MyExtensionMethods
{
}
}
扩展方法:FilterCollectionsBasedOnType
非泛型集合类可以包含不同类型的对象。我们的第一个扩展方法适用于 IEnumerble
接口,它允许您根据给定类型筛选集合。代码如下:
public static System.Collections.Generic.IEnumerable<T>
FilterCollectionsBasedOnType<T>(this System.Collections.IEnumerable list)
where T : class
{
System.Collections.Generic.List<T> filteredList = new List<T>();
foreach (System.Object obj in list)
{
if (obj is T)
filteredList.Add(obj as T);
}
return filteredList;
}
示例
System.Collections.ArrayList myArray = new ArrayList();
myArray.Add(new Car());
myArray.Add(new Box());
myArray.Add(new Person());
IEnumerable<Car> enumerable = myArray.FilterCollections<Car>();
扩展方法:FilterCollectionsBasedOnPredicate
以下扩展方法根据给定谓词委托的结果筛选集合:
public static System.Collections.IEnumerable
FilterCollectionsBasedOnType<T>(this System.Collections.Generic.IEnumerable<T> list,
Predicate<T> predicate)
{
System.Collections.Generic.List<T> filteredList = new List<T>();
foreach (T item in list)
{
if (predicate(item))
filteredList.Add(item);
}
return filteredList;
}
示例
System.Collections.Generic.List<int> numbersList = new List<int>();
numbersList.Add(23);
numbersList.Add(45);
numbersList.Add(56);
numbersList.Add(87);
IEnumerable evenNumbers= numbersList.FilterCollectionsBasedOnPredicate(IsEven)
//OR
IEnumerable evenNumbers= numbersList.FilterCollectionsBasedOnPredicate(num=> num%2 == 0)
//(For more information about lambda expression read my blog).
public bool IsEven(int num)
{
return num % 2 == 0;
}
扩展方法:FindMinimum
FindMinimum
扩展方法适用于整数数组,并返回最小值。
public static int FindMinimum(System.Collections.Generic.IEnumerable<int> list)
{
int min = int.MinValue
foreach (int item in list)
{
if (item < min) min = item;
}
return min;
}
示例
Int[] myIntArray = new int[]{3, 1, 45, 67};
Int minimumValue = myIntArrau.FindMinimum();
扩展方法:FindMaximum
FindMaximum
扩展方法适用于整数数组,并返回最大值。
public static int FindMaximum(System.Collections.Generic.IEnumerable<int> list)
{
int max = int.MaxValue;
foreach (int item in list)
{
if (item > max) max = item;
}
return max;
}
示例
Int[] myIntArray = new int[]{3, 1, 45, 67};
Int maximumValue = myIntArrau.FindMaximum();
注意:您可以为其他数字类型(如 float、double 等)编写重载方法。
LINQ(语言集成查询语言)
您会惊讶地发现您已经涵盖了 LINQ 的核心基础知识。LINQ 主要暴露了一个包含数百个扩展方法的库,就像我们之前练习中构建的那些。以下代码使用了其中一些扩展方法:
以下代码从整数数组中查找最小值:
int[] myIntArray = new int[] { 71, 45, 67, 23, 89, 101 };
int minimumValue = myIntArray.Min();
以下代码从整数数组中查找平均值:
int[] myIntArray = new int[] { 71, 45, 67, 23, 89, 101 };
double averageValue = myIntArray.Average();
以下代码使用委托查找年龄超过 11 岁的学生:
System.Collections.Generic.List<Student> studentCollection = new List<Student>();
Student student1 = new Student();
student1.Name = "Alpha";
student1.Age = 14;
studentCollection.Add(student1);
Student student2 = new Student();
student2.Name = "Beta";
student2.Age = 13;
studentCollection.Add(student2);
Student student3 = new Student();
student3.Name = "Gamma";
student3.Age = 10;
studentCollection.Add(student3);
IEnumerable<Student> enumerable =
studentCollection.Where<Student>(IsStudentOlderThanEleven);
foreach (Student student in enumerable)
{
MessageBox.Show(student.Name);
}
private bool IsStudentOlderThanEleven(Student student)
{
return student.Age > 11;
}
以下代码使用 Lambda 表达式查找年龄超过 11 岁的学生:
System.Collections.Generic.List<Student> studentCollection = new List<Student>();
Student student1 = new Student();
student1.Name = "Alpha";
student1.Age = 14;
studentCollection.Add(student1);
Student student2 = new Student();
student2.Name = "Beta";
student2.Age = 13;
studentCollection.Add(student2);
Student student3 = new Student();
student3.Name = "Gamma";
student3.Age = 10;
studentCollection.Add(student3);
IEnumerable<Student> enumerable =
studentCollection.Where<Student>(student=> student.Age>11);
foreach (Student student in enumerable)
{
MessageBox.Show(student.Name);
}
以下代码使用 Lambda 表达式和 var
查找年龄超过 11 岁的学生:
System.Collections.Generic.List<Student> studentCollection = new List<Student>();
Student student1 = new Student();
student1.Name = "Alpha";
student1.Age = 14;
studentCollection.Add(student1);
Student student2 = new Student();
student2.Name = "Beta";
student2.Age = 13;
studentCollection.Add(student2);
Student student3 = new Student();
student3.Name = "Gamma";
student3.Age = 10;|
studentCollection.Add(student3);
var enumerable=studentCollection.Where<Student>(student=> student.Age>11);
foreach (Student student in enumerable)
{
MessageBox.Show(student.Name);
}
LINQ 提供了泛型和非泛型版本的扩展方法。我建议您浏览并了解 LINQ 引入的其他扩展方法。通过引入更简单、类似 SQL 的语法来调用这些扩展方法,C# 编译器进一步简化了这些扩展方法的使用。
以下代码使用 LINQ 查询语法查找年龄超过 11 岁的学生:
System.Collections.Generic.List<Student> studentCollection = new List<Student>();
Student student1 = new Student();
student1.Name = "Alpha";
student1.Age = 14;
studentCollection.Add(student1);
Student student2 = new Student();
student2.Name = "Beta";
student2.Age = 13;
studentCollection.Add(student2);
Student student3 = new Student();
student3.Name = "Gamma";
student3.Age = 10;
studentCollection.Add(student3);
var enumerable = from student in studentCollection where student.Age > 11 select student;
foreach (Student student in enumerable)
{
MessageBox.Show(student.Name);
}
在编译期间,C# 编译器会将查询语法更改为扩展方法调用。下表说明了 LINQ 的查询结构:
|
定义需要过滤的容器 |
|
过滤条件 |
|
从容器中选择一个对象 |
|
执行 |
|
对结果进行排序 |
|
按给定键对数据进行分组 |
请记住,每个 LINQ 查询最终都会转换为对多个扩展方法的调用。