65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ 内部

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.14/5 (15投票s)

2008年6月17日

CPOL

9分钟阅读

viewsIcon

37101

本文介绍了 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 };

编译器转换为 int[] myIntegerArray = new int[] { 1, 2, 3, 4, 6, 6, 7, 8 };

var myGenericListOfTypeMyClass = 
    new List<MyClass>();

编译器转换为 List<MyClass> myGenericListOfTypeMyClass = new List<MyClass>();

var myClass = new MyClass();

编译器转换为 MyClass 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 };
}

在上面的示例中,我们创建了一个具有这些成员的匿名类型:ColorWeightHeightWidth。与隐式类型变量类似,CLR 没有任何匿名类型的概念。在编译期间,C# 编译器声明一个具有唯一名称的类,并具有四个 readonly 属性 ColorBlueHeightWidth。隐式类型变量 BoxAnonymousType 使用编译器生成的类进行初始化。如果您通过 ILDASM 或 Reflector 反汇编上面的代码,您将看到编译器生成的类。

所有匿名类型的编译器生成类都派生自 System.Object 并覆盖了 EqualGetHashCodeToString 方法。匿名类型的编译器生成类的 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 的查询结构:

from, in

定义需要过滤的容器

其中

过滤条件

select

从容器中选择一个对象

join, on, equals, into

执行 join

orderby, ascending, descending

对结果进行排序

group, by

按给定键对数据进行分组

请记住,每个 LINQ 查询最终都会转换为对多个扩展方法的调用。

© . All rights reserved.