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






4.86/5 (19投票s)
CLinq 项目是一个库,它使得可以通过 C++/CLI 语言使用 LINQ 技术
引言
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^ |
比较:!= , == ;连接:+ ;标准字符串方法(IndexOf 、Substring 等) |
有关支持类型的完整列表以及方法和运算符列表,请参阅生成的文档 (14.9 Kb)。以下示例演示了如何将重载运算符与表示 double
和 float
的表达式一起使用。混合不同类型是另一个有趣的问题,这就是为什么我们在这里使用两种不同的浮点类型
// 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 仅允许从较小的浮点数据类型到较大的浮点数据类型进行隐式转换——即 float
到 double
——或者从较小的整数类型到较大的整数类型,例如 short
到 int
。此示例还使用了 Expr<Math^>
类,这是另一个有趣的模板特化。此特化表示 .NET System::Math
类,并包含此类的_大部分_方法。
使用类
我之前已经演示了如何使用 int
或 float
等基本数据类型,但我很少提及如何使用其他类。有两种可能的方法:如果存在模板特化,您可以使用它,它包括表示底层类成员的属性和方法。这些特化存在于某些标准类型,如 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^>
类中声明的两个方法。这些方法是 IndexOf
和 Substring
;它们表示对 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 中的查询。该类有几种构造查询的方法,包括 Where
、Select
、Average
等。如果您已经有一个实现 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; }
}
};
该类有两个属性——ID
和 Name
——一个无参数构造函数和一个需要进一步解释的构造函数。该构造函数接受两个参数,用于初始化类的两个字段。每个参数上还附加了一个名为 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
是一个非常简单的泛型类,有两个类型参数,包含两个属性——称为 First
和 Second
——它们的类型由类型参数决定。如果您想从投影或连接返回两个不同的值而无需声明自己的类,则可以使用此类。
查询从投影返回 Tuple
类型,然后使用 Where
操作过滤客户。这揭示了使用预定义 Tuple
类的一个优点:类型为表示元组 Expr<Tuple<>^>
的表达式的 co
变量作为参数传递给 lambda 表达式。在 lambda 表达式中,我们可以直接使用它的属性 First
和 Second
。因为我们正在操作表达式,所以我们不是直接使用 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 主文章库