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

CLinq - C++/CLI 语言的 LINQ 支持

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (19投票s)

2007年3月2日

Ms-PL

19分钟阅读

viewsIcon

66181

downloadIcon

1118

CLinq 项目是一个库,它使得可以通过 C++/CLI 语言使用 LINQ 技术

访问 CodePlex 上的项目主页.

引言

LINQ 项目将作为 Visual Studio 下一个版本(代号“Orcas”)的一部分,它是一组扩展,使得可以直接从 C# 或 VB.NET 语言查询数据源。LINQ 通过表示查询的类以及 C# 和 VB.NET 语言的特性扩展了 .NET Framework,从而可以轻松编写这些查询。它还包括用于与最常见数据源(如 SQL 数据库、DataSet 和 XML 文件)一起使用查询的库。本文需要一些 LINQ 和 C# 3.0 的基本知识,因此我建议在阅读本文之前查阅官方项目网站上提供的 LINQ 概述。

LINQ 包含 C# 和 VB.NET 的扩展,但目前没有在 C++/CLI 中支持 LINQ 的计划。CLinq 项目的目标是允许在 C++/CLI 中使用 LINQ 的部分功能。多亏了 C++/CLI 中非常强大的运算符重载机制,才能在 C++/CLI 中启用 LINQ to SQL 来访问 SQL 数据库,以及一些其他 LINQ 用法。我将首先演示相同的数据库查询在 C# 3.0 和 C++/CLI 中的样子。然后我们将更详细地了解 CLinq。以下查询用 C# 3.0 编写,使用 Northwind 数据库并返回所有居住在伦敦的客户的联系人姓名和公司名称

// create connection to database
NorthwindData db = new NorthwindData(".. connection string ..");

// declare database query
var q = 
    from cvar in db.Customers
    where cvar.City == "London"
    select cvar.ContactName + ", " + cvar.CompanyName;

// execute query and output results
foreach(string s in q)
    Console.WriteLine(s);

现在,让我们看看使用 CLinq 在 C++/CLI 中编写的相同查询。它稍微复杂一些,但这是将其作为库而不是修改语言来实现的代价

// create connection to database
NorthwindData db(".. connection string ..");

// declare database query
Expr<Customers^> cvar = Var<Customers^>("c");
CQuery<String^>^ q = db.QCustomers
    ->Where(clq::fun(cvar, cvar.City == "London"))
    ->Select(clq::fun(cvar, 
        cvar.ContactName + Expr<String^>(", ") + cvar.CompanyName));

// execute query and output results
for each(String^ s in q->Query)
    Console::WriteLine(s);

LINQ 和 C++/CLI 概述

在本节中,我将非常简要地回顾一些对理解 CLinq 工作原理很重要的 LINQ 和 C++/CLI 功能。如果您熟悉 LINQ 和 C++/CLI,可以安全地跳过本节。

重要的 LINQ 功能

C# 中最能使 LINQ 成为可能的重要扩展可能是 Lambda 表达式。Lambda 表达式类似于匿名委托,但语法更简单。Lambda 表达式可用于内联声明函数,并且您可以将它们作为参数传递给方法。然而,与匿名委托有一个重要的区别:Lambda 表达式既可以编译为可执行代码(如匿名委托),也可以编译为表示 Lambda 表达式源代码的数据结构。该结构称为表达式树。表达式树也可以在运行时编译,因此您可以将此表示形式转换为可执行代码。

LINQ to SQL 所做的是获取表示包含 lambda 表达式的查询的表达式树,并将其转换为 SQL 查询,然后发送到 SQL Server。LINQ to SQL 还包含一个名为 sqlmetal.exe 的工具,它生成表示数据库结构的对象。因此,当您编写查询时,您可以使用这些类型安全的对象,而无需按名称指定数据库表或列。

重要的 C++/CLI 功能

现在我想提一下 C++/CLI 丰富功能集中的几个。LINQ 本身适用于 .NET,因此在整个项目中我们将大量使用与托管类一起工作的能力。我们还将使用与 C++ 模板和 .NET 泛型一起工作的能力。CLinq 受益于 .NET 泛型可以从程序集编译和导出这一事实,而 C++ 模板由于其模板特化支持而变得有趣。这意味着,如果您有一个 SomeClass<T> 模板,您可以为指定的类型参数编写一个特殊版本——例如 SomeClass<int>——并修改此类的行为,包括添加方法的可能性等等。

CLinq 基本功能

在前面的示例中,我们使用了 Expr<Customers^>Var<Customers^> 类。这两个类是类型化包装器,使用 C++/CLI 模板声明。我们使用模板而不是泛型,因为模板允许我们使用模板特化。这意味着存在基本的 Expr<>Var<> 类,并且它们可以被特化。例如,Expr<Customers^> 可以包含一些额外的属性。使用这些额外的属性,您可以表达与 Customers 类的操作。这些模板特化可以使用 clinqgen.exe 工具生成,该工具将在后面描述。CLinq 还支持更复杂的语法,您可以使用它来操作没有模板特化的类。

在我们开始之前,我将解释 CLinq 库的组织方式。它由两部分组成。第一部分是 EeekSoft.CLinq.dll 程序集,其中包含核心 CLinq 类。您需要使用项目设置或 #using 语句从您的项目中引用此程序集。第二部分是 clinq.h 头文件和另外两个包含 C++/CLI 模板的头文件。您需要在每个 CLinq 项目中包含此头文件。使用头文件是因为 CLinq 依赖于 C++/CLI 模板。如果您想在更多 .NET 项目之间共享 CLinq 对象,可以使用核心库中的类。

我之前提到了 Expr<> 类。这个类是用模板编写的,它与 Var<> 一起包含在 clinq.h 文件中。这两个类都继承自 CLinq 程序集中的类,即 Expression<>。程序集中还有一些其他类,但这个是最重要的。这个类可以在多个项目中共享,它是用 .NET 泛型编写的。建议将这个类用作您项目中任何可以从其他 .NET 程序集调用的公共方法的参数类型。

Expr 和 Var 类

让我们看一些示例代码。正如您从上一段中看到的,Expr<>Var<> 类是 CLinq 项目的关键结构,因此我们将在以下示例中使用它们。该示例使用这两个类的两个专用版本,一个用于 int 类型,另一个用于 String^ 类型

// Declare variable of type int called 'x'
Expr<int> x = Var<int>("x");

// Declare expression of type String initialized with literal
Expr<String^> str("Hello world!");

// Expression representing addition of the x variable and 
// result of the method call to 'IndexOf' method.
Expr<int> expr = str.IndexOf("w") + x;

如果你查看代码,你可能会认为 IndexOf 方法和其他操作是在代码调用后执行的,但这不是真的!这是一个重要的事实需要注意:代码只构建表示表达式的内部结构,但**表达式并没有被执行!**这为您提供了类似于 C# 3.0 lambda 表达式的行为,后者也可以用于构建所编写表达式的表示,而不是构建可执行代码。您还可以将由 Expr<> 类表示的表达式转换为 LINQ 使用的结构,如下例所示

// Convert to LINQ expression
System::Expressions::Expression^ linqExpr = expr.ToLinq();
// Print string representation of LINQ expression
Console::WriteLine(linqExpr);

打印到控制台窗口的结果将是

Add("Hello world!".IndexOf("w"), x)

Lambda 表达式

现在让我们看看在 CLinq 中编写 lambda 表达式的语法。Lambda 表达式由泛型 Lambda<> 类表示。这个类的类型参数应该是 LINQ 在 System::Query 命名空间中声明的 Func 委托之一。要声明 lambda 表达式,您可以使用 EeekSoft::CLinq::clq 命名空间中的 fun 函数。假设您包含了 using namespace EeekSoft::CLinq; 指令(建议这样做),源代码将如下所示

// Declare parameter (variable) and method body (expression)
Expr<int> var = Var<int>("x");
Expr<int> expr = Expr<String^>("Hello world!").IndexOf("w") + var;

// First argument for the clq::fun function is lambda expression
// parameter, the last argument is the lambda expression body
Lambda<Func<int, int>^>^ lambda = clq::fun(var, expr);

// Print string representation of lambda..
Console::WriteLine(lambda->ToLinq());

// Compile & execute lambda
Func<int, int>^ compiled = lambda->Compile();
Console::WriteLine(compiled(100));

执行此示例后,您应该在控制台窗口中看到以下输出。第一行表示 lambda 表达式,第二行是 lambda 表达式调用的结果

x => Add("Hello world!".IndexOf("w"), x)
106

与 LINQ 类似,您可以在运行时编译 CLinq 表达式。实际上,CLinq 内部使用 LINQ。这在前面的示例中是使用 Compile 方法完成的。返回类型是 Func<> 委托之一,并且此委托可以直接调用。

与 LINQ 中一样,lambda 表达式中最多只能使用 4 个参数。这是由于 LINQ 程序集中声明的 Func<> 委托的限制。因此,clq::fun 函数具有相同数量的重载。另请注意,在大多数情况下,您不必为此函数指定类型参数,因为 C++/CLI 类型推断算法可以为您推断类型。让我们看一个演示声明具有多个参数的 lambda 表达式的示例

Expr<int> x = Var<int>("x");
Expr<int> y = Var<int>("y");
Lambda<Func<int, int, int>^>^ lambda2 = 
    clq::fun(x, y, 2 * (x + y) );
Console::WriteLine(lambda2->Compile()(12, 9));

在这个例子中,lambda 表达式的主体没有作为另一个变量提前声明,而是直接在 clq::fun 函数中组合。我们还在 lambda 表达式的主体中使用了重载运算符,即 *+。如果你运行这段代码,结果将是 (12 + 9) * 2,即 42

支持的类型和运算符

在前面的示例中,我使用了两个重载运算符。这些运算符在 Expr<int> 模板特化中声明。在处理表示整数的表达式时,您可以使用它们。CLinq 包含带有以下标准类型重载运算符的模板特化

类型 支持的运算符和方法
bool 比较:!=, ==;逻辑:&&, ||, !
int 比较:!=, ==, <, >, <=, >=;数学:+, *, /, -;模数:%;移位:<<, >>
其他整数类型 比较:!=, ==, <, >, <=, >=;数学:+, *, /, -;模数:%
float, double, Decimal 比较:!=, ==, <, >, <=, >=;数学:+, *, /, -
wchar_t 比较:!=, ==
String^ 比较:!=, ==;连接:+;标准字符串方法(IndexOfSubstring 等)

有关支持类型的完整列表以及方法和运算符列表,请参阅生成的文档 (14.9 Kb)。以下示例演示了如何将重载运算符与表示 doublefloat 的表达式一起使用。混合不同类型是另一个有趣的问题,这就是为什么我们在这里使用两种不同的浮点类型

// Declare 'float' variable and 'double' literal
Expr<float> fv = Var<float>("f");
Expr<double> fc(1.2345678);

// Function taking 'float' and returning 'float'
Lambda<Func<float, float>^>^ foo4 = clq::fun(fv, 
    clq::conv<float>(Expr<Math^>::Sin(fv * 3.14) + fc)  );

您可以看到我们正在使用 clq 命名空间中的另一个函数,clq::conv。此函数用于在隐式转换不可用时转换类型。在示例中,我们使用接受 Expr<double> 作为参数的 Sin 函数。float 类型的变量被隐式转换为 double 类型的表达式,但当反方向转换不可能时,我们必须使用 clq::conv 函数。CLinq 仅允许从较小的浮点数据类型到较大的浮点数据类型进行隐式转换——即 floatdouble——或者从较小的整数类型到较大的整数类型,例如 shortint。此示例还使用了 Expr<Math^> 类,这是另一个有趣的模板特化。此特化表示 .NET System::Math 类,并包含此类的_大部分_方法。

使用类

我之前已经演示了如何使用 intfloat 等基本数据类型,但我很少提及如何使用其他类。有两种可能的方法:如果存在模板特化,您可以使用它,它包括表示底层类成员的属性和方法。这些特化存在于某些标准类型,如 String^,并且可以为 LINQ to SQL 数据库映射生成。如果模板特化不可用,您必须使用可以通过名称调用方法或属性的通用方法。

类型化包装器

如果存在相应的模板特化,则使用类相当简单。以下示例声明了一个使用 String^ 类型的表达式

// 'String^' variable
Expr<String^> name = Var<String^>("name");

// Expression that uses 'IndexOf' and 'Substring' methods
Expr<String^> sh("Hello Tomas");
Expr<int> n = sh.IndexOf('T');
Lambda<Func<String^, String^>^>^ foo = 
    clq::fun(name, sh.Substring(0, n) + name);

// Print LINQ representation and execute
Console::WriteLine(foo->ToLinq());
Console::WriteLine(foo->Compile()("world!"));

在此示例中,我们使用了 Expr<String^> 类中声明的两个方法。这些方法是 IndexOfSubstring;它们表示对 String^ 类型的相应方法的调用。如果您查看程序输出,您会看到它包含对这两个方法的调用。还有一个对 Concat 方法的调用,该方法是我们使用 + 运算符进行字符串连接时由 CLinq 生成的

name => Concat(new [] {"Hello Tomas".
    Substring(0, "Hello Tomas".IndexOf(T)), name})
Hello world!

间接成员访问

为了演示第二种方法,我们首先定义一个新类,其中包含一个示例属性、方法和静态方法。您还可以调用静态属性

// Sample class that we'll work with
ref class DemoClass
{
    int _number;
    public:
    DemoClass(int n) 
    { 
        _number = n; 
    }  
    // Property
    property int Number 
    {
        int get() 
        { 
            return _number;  
        }
    }
    // Standard method
    int AddNumber(int n) 
    {
        return _number = _number + n;
    }
    // Static method
    static int Square(int number) 
    {
        return number * number;
    }
};

现在让我们进入示例中更有趣的部分。我们将首先声明一个 DemoClass^ 类型的变量,然后我们将使用 Prop 方法按名称读取属性。我们将使用 Invoke 调用成员方法,使用 InvokeStatic 调用此类的静态方法。AddNumber 方法可能有点棘手,因为它以副作用的形式增加类中存储的数字,这意味着表达式的值取决于表达式成员的评估顺序

// Construct the lambda expression
Expr<DemoClass^> var = Var<DemoClass^>("var");
Lambda<Func<DemoClass^,int>^>^ foo = clq::fun(var, 
    var.Prop<int>("Number") + 
    var.Invoke<int>("AddNumber", Expr<int>(6)) + 
    Expr<DemoClass^>::InvokeStatic<int>("Square", Expr<int>(100) ) );

// Compile the lambda and pass instance of 'DemoClass' as a parameter
DemoClass^ dcs = gcnew DemoClass(15);
int ret = foo->Compile()(dcs);

Console::WriteLine("{0}\n{1}", foo->ToLinq(), ret);

此示例的输出将是

var => Add(Add(var.Number, var.AddNumber(6)), Square(100))
10036

我之所以包含输出,是因为我想指出一个有趣的事实。您可以看到,无论您使用生成的模板特化还是按名称调用,输出都没有区别。这是因为如果您使用按名称调用,在生成 LINQ 表达式树之前,将使用反射找到要调用的方法或属性。这也意味着,如果您执行编译后的 lambda 表达式,它将直接调用方法或属性,而不是按其名称调用。

在投影中调用构造函数

到目前为止,我们已经了解了如何调用方法和读取属性值。还有一个我没有写到的有趣问题。有时您可能想创建一个类的实例并从 lambda 表达式中返回它。CLinq 不支持 C# 3.0 的匿名方法之类的东西,但是您可以使用 clq::newobj 函数调用类构造函数并向其传递参数。以下示例假设您有一个名为 DemoCtor 的类,其构造函数接受 String^int 作为参数

// Arguments of the lambda expression
Expr<String^> svar = Var<String^>("s");
Expr<int> nvar = Var<int>("n");

DemoCtor^ d = clq::fun(svar, nvar, clq::newobj<DemoCtor^>(svar, nvar) )
    ->Compile()("Hello world!", 42);

执行此代码后,d 变量将包含一个使用我前面提到的构造函数创建的 DemoCtor 类的实例。使用 newobj 方法时应非常小心,因为它没有编译时检查。因此,如果所需的构造函数不存在或类型不兼容,代码将以运行时错误结束。

使用 LINQ

您现在已经熟悉了所有 CLinq 功能,可以开始在 C++/CLI 中使用 LINQ 处理数据了!处理数据的关键是 CQuery 类。它充当 IQueryable 接口的 CLinq 包装器,该接口表示 LINQ 中的查询。该类有几种构造查询的方法,包括 WhereSelectAverage 等。如果您已经有一个实现 IQueryable 接口的类的实例,则可以构造此类的实例,但对于处理数据库,您可以使用工具生成简化代码。CQuery 类还有一个名为 Query 的属性,它返回底层的 IQueryable 接口。我们稍后需要此属性来访问查询结果。

使用 SQL 数据库

LINQ to SQL:简介

我们将使用两个工具来生成一个包含表示数据库结构的类的 CLinq 头文件。第一个工具作为 LINQ 的一部分提供,称为 sqlmetal。此工具可以生成 C# 或 VB.NET 代码,但也可以用于生成数据库结构的 XML 描述。我们将使用第三个选项:以下示例演示如何为在 localhost 运行的 SQL 服务器上的 Northwind 数据库生成 XML 描述 northwind.xml

sqlmetal /server:localhost /database:northwind /xml:northwind.xml

一旦我们有了 XML 文件,我们就可以使用 CLinq 的 clinqgen 工具。这个工具会生成 C++/CLI 头文件,其中包含根据 Expr<> 模板特化表示数据库表的类,以及表示整个数据库的类。您可以自定义这个类的名称和命名空间。如果您想自动化这个任务,可以将 sqlmetal 生成的 XML 文件包含在您的项目中,并将其自定义构建工具设置为以下命令。黑客注意:您还可以使用管道 (|) 将这两个工具一起使用。

clinqgen /namespace:EeekSoft.CLinq.Demo 
    /class:NorthwindData /out:Northwind.h $(InputPath)

现在您需要包含生成的头文件,然后我们就可以开始使用数据库了。我们首先将创建生成的 NorthwindData 类的一个实例,它表示数据库。请注意,该示例使用 C++/CLI 栈语义,但如果您愿意,也可以使用 gcnew。一旦我们有了这个类的一个实例,我们就可以使用它的表示数据表的属性。带有 Q 前缀的属性返回 CQuery 类。因此,我们将使用这些属性而不是没有此前缀的属性,后者是为 C# 3.0 或 VB.NET 使用而设计的。以下示例演示了一些基本的 CQuery 方法

// Create database context
NorthwindData db(".. connection string ..");

// (1) Count employees
Console::WriteLine("Number of employees: {0}",
    db.QEmployees->Count());

// (2) Calculate average 'UnitPrice' value
Expr<Products^> p = Var<Products^>("p");
Nullable<Decimal> avgPrice = 
    db.QProducts->Average( clq::fun(p, p.UnitPrice) );
Console::WriteLine("Average unit price: {0}", avgPrice);

// (3) Get first employee whose 'ReportsTo' column is NULL
Expr<Employees^> e = Var<Employees^>("e");
Employees^ boss = db.QEmployees->
    Where( clq::fun(e, e.ReportsTo == nullptr) )->First();
Console::WriteLine("The boss: {0} {1}",
    boss->FirstName, boss->LastName);

在第一个示例中,我们只是调用了 Count 方法,它返回表中的行数。在第二个示例中,我们使用了 Average 方法,它需要一个参数,该参数是一个 lambda 表达式,用于返回表中每行的数值类型。由于 UnitPrice 列可以包含 NULL 值,我们正在使用 Nullable<Decimal> 类型。它既可以包含实际值,也可以包含 NULL,在 C++/CLI 中使用 nullptr 表示。第三个示例使用 Where 方法仅筛选匹配指定谓词(即 lambda 表达式)的行。此调用的结果也是 CQuery 类,因此我们可以轻松地连接多个操作。在此示例中,我们附加了一个对 First 方法的调用,该方法返回结果集中的第一行。

LINQ to SQL:过滤与投影

让我们看一个更有趣的示例,它涵盖了过滤(即 Where 方法)和投影(即 Select 方法)。查询的结果将是一个包含名为 CustomerInfo 的自定义类实例的集合。那么,让我们首先看看这个类

ref class CustomerInfo
{
    String^ _id;
    String^ _name;

    public:
    CustomerInfo([PropMap("ID")] String^ id, 
        [PropMap("Name")] String^ name) 
    { 
        _id=id; _name=name; 
    }
    CustomerInfo() { }

    property String^ ID 
    { 
        String^ get() { return _id; }
        void set(String^ value) { _id = value; }
    }

    property String^ Name 
    {
        String^ get() { return _name; }
        void set(String^ value) { _name = value; }
    }
};

该类有两个属性——IDName——一个无参数构造函数和一个需要进一步解释的构造函数。该构造函数接受两个参数,用于初始化类的两个字段。每个参数上还附加了一个名为 PropMap 的属性,它描述了构造函数如何初始化类的属性。例如,附加到 id 参数的属性 [PropMap("ID")] 意味着 ID 属性的值将在构造函数中设置为 id 参数的值。

为什么这些信息很重要?首先,它不会用于以下查询,但是您可以编写一个查询,该查询构造一个 CustomerInfo 对象集合,然后使用 Where 方法过滤此集合。整个查询将传递给 LINQ 进行转换为 SQL。如果您使用 ID 属性进行过滤,LINQ 需要知道之前分配给此属性的值。因此,CLinq 具有 PropMap 属性,该属性将属性值映射到之前传递给构造函数的参数。在 C# 3.0 中,行为略有不同,因为您可以使用匿名类型,并且无需直接将值传递给构造函数。

// DB context & variable.. 
NorthwindData db(".. connection string ..");
Expr<Customers^> cvar = Var<Customers^>("c");

// Query: select some information about customers living
//   in country whose name starts with the letter "U"
CQuery<CustomerInfo^>^ q = db.QCustomers
    ->Where(clq::fun(cvar, cvar.Country.IndexOf("U") == 0))
    ->Select(clq::fun(cvar, clq::newobj<CustomerInfo^>(
        cvar.CustomerID, cvar.ContactName + 
        Expr<String^>(" from ") + cvar.Country)));

// Print SQL command sent to SQL server
Console::WriteLine("\nQuery:\n{0}\n\nResults:", 
    q->Query->ToString());

// Print returned rows
for each(CustomerInfo^ c in q->Query)
    Console::WriteLine(" * {0},  {1}", c->ID, c->Name);

这段代码与您在 C# 3.0 中使用 LINQ 时通常编写的代码非常相似。在此示例中,我们首先创建数据库上下文并声明一个将在查询中使用的变量。查询本身获取表示数据库中 Customers 表的 QCustomers 属性。然后,它使用 Where 方法过滤来自以字母“U”开头的国家的客户。最后,它执行投影(即 Select 方法),其中只选择我们感兴趣的信息并创建 CustomerInfo 对象。

该示例还打印将从查询生成的 SQL 命令。如果您在表示查询的 IQueryable 上调用 ToString 方法,LINQ 将返回 SQL 命令。正如我之前提到的,CQuery 类的底层 IQueryable 可以通过 Query 属性访问。因此,代码 q->Query->ToString() 返回 SQL 命令。代码所做的最后一件事是执行查询并打印所有返回客户的信息。当您开始枚举集合时,查询会自动执行,这在 for each 语句中完成。

LINQ to SQL:连接和元组

对于最后一个例子,我编写了一个更复杂的查询。它首先对客户和订单执行 GroupJoin 操作,这意味着它返回一个包含客户及其所有订单的元组集合。在此连接之后,它执行 Where 过滤,并且只返回至少有一个订单将运往美国的客户。客户仍然与他们的订单一起保留。查询执行的最后一个操作是一个投影,它生成一个包含公司名称及其关联订单数量的字符串。

这个查询还演示了一些我们之前不需要的更有趣的东西。示例以两个 typedef 开头,以使代码更具可读性。第一个只是定义了订单集合的快捷方式。第二个使用 Tuple 类,这是 CLinq 的一部分,我还没有提到。Tuple 是一个非常简单的泛型类,有两个类型参数,包含两个属性——称为 FirstSecond——它们的类型由类型参数决定。如果您想从投影或连接返回两个不同的值而无需声明自己的类,则可以使用此​​类。

查询从投影返回 Tuple 类型,然后使用 Where 操作过滤客户。这揭示了使用预定义 Tuple 类的一个优点:类型为表示元组 Expr<Tuple<>^> 的表达式的 co 变量作为参数传递给 lambda 表达式。在 lambda 表达式中,我们可以直接使用它的属性 FirstSecond。因为我们正在操作表达式,所以我们不是直接使用 Tuple 类。相反,我们正在使用 Expr 类的模板特化,其中 Expr<Tuple<>^> 被扩展以包含这两个属性。我将在后面评论此示例中使用的其他有趣功能,所以现在让我们看一下查询

// First declare type for storing Customer and her Orders
typedef IEnumerable<Orders^> OrdersCollection;
typedef Tuple<Customers^, OrdersCollection^> CustomerOrders;

// Connect to DB and declare variables
NorthwindData db(".. connection string ..");
Expr<Customers^> c = Var<Customers^>("c");
Expr<Orders^> o = Var<Orders^>("o");
Expr<OrdersCollection^> orders
    = Var<OrdersCollection^>("orders");
Expr<CustomerOrders^> co = Var<CustomerOrders^>("co");

// The Query
CQuery<String^>^ q = db.QCustomers
    // Group customers and their orders and 
    // produce collection of 'CustomerOrders'
    ->GroupJoin(db.QOrders,
        clq::fun(c, c.CustomerID),
        clq::fun(o, o.CustomerID),
        clq::fun<Customers^, OrdersCollection^, CustomerOrders^>
        ( c, orders, clq::newobj<CustomerOrders^>(c, orders) ))
    // Filter only customers with order shipped to USA 
    // Note: 'Second' is the collection with orders
    ->Where( clq::fun(co, co.Second.Where( 
        clq::fun(o, o.ShipCountry == "USA" )).Count() > 0) )
    // Projection - string concatenation
    ->Select( clq::fun(co, 
        co.First.CompanyName + Expr<String^>(", #orders = ") + 
        Expr<Convert^>::ToString(co.Second.Count()) ) );

让我们关注 Where 子句。lambda 表达式接受一个 Tuple 类型的表达式作为参数(我前面解释过),并访问其第二个值 co.Second。此参数的类型是表示集合的表达式,Expr<IEnumerable<>^>。这是 Expr<> 类的另一个特化,通过 InteliSense,您可以发现此类包含许多用于处理集合的方法!这些方法对应于 CQuery 类中可用的方法,但旨在处理表示查询的表达式,而不是直接处理查询。在此示例中,我们使用 Where 方法,该方法再次返回表示查询的表达式以及 Count 方法。

之前没有提到的第二个类是 Expr<Convert>,它只是另一个类似于 Expr<Math> 的模板特化。它包含几种类型转换方法。在此示例中,我们使用 ToString 方法将订单数量转换为字符串。

项目摘要

目前,该项目处于非常早期的阶段。这意味着它需要更多的测试以及其他人的审查。如果您发现任何错误,或者如果您认为 CLinq 缺少一些重要的 LINQ 功能,请告诉我。该项目目前使用 LINQ 2006 年 5 月 CTP 版本,但一旦更稳定的 Beta 版本可用,它将更新以支持 Visual Studio “Orcas”。该项目可在 CodePlex [^] 上获取,因此您可以从项目网站下载最新版本的源代码和二进制文件。因为我不是 C++/CLI 专家,所以我非常感兴趣您的评论和建议。此外,如果您愿意参与该项目,请告诉我!

版本和更新

  • (2007年3月2日)第一个版本,使用 LINQ 2006年5月 CTP
  • (2007年7月27日)文章已编辑并移至 CodeProject.com 主文章库
© . All rights reserved.