LINQ to CSV,一种类型安全、动态、高性能的方法






4.89/5 (10投票s)
LINQ to CSV 用于使用 C# 的另一种动态类型进行快速应用程序开发。
引言
语言集成查询 (LINQ) 提供了一个出色的接口来查询以各种形式存储的数据对象。LINQ to SQL、LINQ to Objects、LINQ to XML 是一些例子。LINQ 为查询任何类型的数据源提供了通用接口。由于 LINQ 查询已集成到 C# 语言中,因此可以加快开发速度。本文介绍了一种用于 LINQ to CSV(逗号分隔)文件数据的方法。
背景
主要设计目标是促进快速业务应用程序开发。考虑您有一个包含业务数据的 CSV 文件。您需要编写一个功能,例如 Business_Feature1
,以消耗此数据进行聚合。现在,在一天或两天内开发此类 Business_Feature1
并不难。然后,过几天,假设您需要编写 Business_Feature2
来消耗另一个 CSV 文件以提供顶级销售区域。同样,开发人员需要花费另外 1 到 2 天来开发此功能。最聪明的做法是在开发 Business_Feature1
时分离关注点。也就是说,Business_Feature1
被开发为两个模块。模块 1,消耗 CSV 文件将其转换为对象,而模块 2 在这些对象上执行业务操作以提供简洁的业务功能。这很可能花费相同的时间。但是,如果遵循这种智能方法,开发 Business_Feature2
将变得轻而易举,只需几个小时。这是因为 Business_Feature1
中的模块 1 被重用于快速应用程序开发。这不仅缩短了开发时间,还提高了输出质量。由于模块 1 已经过严格的测试阶段,因此它更稳定且无错误。
现在,有了这个背景,可以想象将该可重用模块(模块 1)编写成 C# 的经典语言特性 LINQ。那么,业务功能的开发将不需要几个小时,而是几分钟,因为您所需要做的就是编写一个简单的 LINQ 查询以获得所需的功能开发。
其他 LINQ 2 CSV 与本库的比较
Using the Code
此 LINQ to CSV 库的主要优点是,开发人员根本不需要编写任何特殊代码。他们可以将库 linq2csv
导入到他们的项目中,只需编写 LINQ 查询即可访问 CSV 文件。所有数据绑定都在运行时发生。
包含一个示例项目来使用此库 (Linq2CSV
)。假设我们有如下 CSV 文件。Region,NA 是北美(not,Not Applicable)。我们的业务目标是按地区查找净利润。
输入 CSV 文件
区域 | 国家 | 账户 ID | 货币 | 毛利润 | 税率 |
不适用 | 美国 | 1 | 美元 | 1234232.76 | 10 |
不适用 | Canada | 2 | CAN | 453232.4576 | 10 |
亚太 | 香港 | 3 | 港元 | 124342.1 | 1 |
欧洲、中东和非洲 | 沙特阿拉伯 | 4 | SAR | 2345234535 | 0 |
有了这个 LINQ to CSV 库,这项任务只需几分钟的开发时间。这是因为此开发所需的行数只有 1 行,即 1 个 LINQ 查询,如下所示
// Method signature looks like public IEnumerable<dynamic> GetNetProfitByRegionUSD()
// LINQ query
result = from data in dataToOperate
group data by ((dynamic)data).region
into profitByRegion
select new
{
Region = profitByRegion.Key,
NetProfitUSD = profitByRegion.SumOfDecimals(dataItem =>
{
decimal netProfit = 0.0M;
decimal tax = 0.0M;
if (((dynamic)dataItem).gross_profit > 0)
tax = ((dynamic)dataItem).gross_profit *
(((dynamic)dataItem).tax_rate / 100.0M);
netProfit = ((dynamic)dataItem).gross_profit - tax;
return netProfit * ((dynamic)dataItem).conversion_factor;
})
};
//
如果您熟悉 LINQ,这会看起来很简单。
输出根据本文给出的 CSV 文件输入如下所示
### Total Net Profit (Tax deducted) by Region in USD ###
Region : NA | NetProfitUSD : 127285002.6245864620 |
Region : APAC | NetProfitUSD : 27333822.56032211400 |
Region : EMEA | NetProfitUSD : 1259879792.90627115830 |
Region : APC | NetProfitUSD : 434647.7561869611525 |
从本文顶部的链接下载示例,以查看快速运行的输出。您可以更改 CSV 文件的内容,以查看示例输出如何响应您的更改。
示例输出如下所示
设计动机
主要设计目标是促进快速业务应用程序开发。为了实现这一设计目标,软件中对于相似用例通用的组件被精确地分离为可重用模块。这使开发团队能够专注于开发业务功能,而样板模块则已准备就绪。
数据从 CSV 文件加载到 C# 动态对象中。这是可以重用的样板组件。CSV 文件内容被加载为对象数组,其中 CSV 标头作为对象属性,即 item[0].gross_profit
将获取数组中的第一个对象,属性名为 gross_profit
。通过一行 C# LINQ 查询即可轻松开发业务功能。该模块本质上是针对 CSV 文件的 LINQ。新功能开发仅涉及为业务场景编写 LINQ 查询。
- 分析了纯 LINQ 与混合 LINQ 之间的权衡(参见权衡部分)
- 基于接口的设计,实现更清晰的抽象
- 足够通用,可以处理任何带有标头的 CSV 文件
- CSV 文件被加载为对象数组,其成员属性可供操作。这使得可以对这些对象编写 LINQ 查询。
类型安全 - 运行时类型安全检查
此库完全类型安全,并检查 CSV 中的每个元素是否符合类型安全。如果发现类型不匹配,则会抛出异常。这有助于用户在处理查询结果时出现错误数据或异常之前立即处理问题。是的,请记住,LINQ 在处理查询结果时具有相当晚的绑定。
CSV 文件通过这种方式将类型信息馈送到 linq2csv
库
它的另一个面貌,动态类型
在 C# 中,动态类型将在运行时解析。动态类型根据 RHS(右侧)的类型在运行时更改其类型。例如,在以下代码中
dynamic runTimeType = "Hello World";
Console.Writeline("Type of runTimeType: {0}", runTimeType.GetType().ToString());
这将打印 System.String
。
如果您将任何类分配给动态类型,编译器将不会检查方法、属性或对该对象的任何操作的正确性。这是因为这是在运行时解析的。动态类型在 Visual Studio IDE 中没有 IntelliSense 支持。但是,尽管没有进行编译时检查,但如果方法或属性不正确,仍会抛出运行时异常。
权衡与性能
- 与其他的 LINQ to CSV 库不同,您甚至不需要编写一行代码来使用您的 CSV 文件。您可以导入库并编写针对 CSV 文件内容的 LINQ 查询。所有绑定都是在运行时神奇地完成的。
- 分析了纯 LINQ 与混合 LINQ 之间的权衡。纯 LINQ 被称为将 LINQ to CSV 功能实现为纯 LINQ 扩展函数。混合 LINQ 被称为通过动态对象实现 LINQ to CSV 功能。由于以下原因,采用了混合 LINQ 方法
- 纯 LINQ 需要为每个查询加载 CSV 文件,而混合 LINQ 只将 CSV 文件加载到内存中一次,供所有查询使用。
- 由于纯 LINQ 查询中存在“C# 闭包”问题,纯 LINQ 效率低下,而混合方法没有闭包问题。
- 使用 .NET 并行库的快速 CSV 文件读取器
- 使用了第三方日志记录器 Log4net。Log4net 功能丰富,适合此类项目
- 使用 LINQ 有助于提高性能,因为避免了不必要的内存分配。只有在读取时才会获得查询结果。
尽量少地编写新的 LINQ 扩展,同时充分利用内置的 LINQ 功能。
文件夹结构
主文件夹包含 bin、doc、sample、source 和 specification。顾名思义,每个目录都包含相应的内容。Specification 目录包含需求文档。
- Bin – 包含发布和调试配置的二进制文件
- Sample – 包含发布和调试模式下的工作示例程序
- Source – 包含库、示例程序和单元测试的源代码
- Specification – 需求文档
- Readme.docx – 包含项目详细信息的说明文件
简要代码走读
这是代码的主要功能部分。令人惊讶的是,仅用一行代码就完成了业务需求。
按国家/地区计算美元总毛利润的单行 LINQ 查询
result = from data in dataToOperate
group data by ((dynamic)data).country
into profitByCountry
select new
{
Country = profitByCountry.Key,
GrossProfitUSD =
profitByCountry.SumOfDecimals(item =>
((dynamic)item).gross_profit *(((dynamic)item).conversion_factor))
};
在这里,dataToOperate
是 CSV 读取器公开的数据结构。该数据按国家/地区分组。然后使用 SumOfDecimals
方法添加 Total
。毛利润通过转换率转换为美元。
局限性与假设
以下是该模块的假设和局限性
- 税率的计算不考虑任何税级或特定于国家的税收规则。
- 对于亏损账户(即利润为负的账户),税率计算为零。
- 未检查输入 CSV 文件项的类型安全,但这是期望的。可以添加此功能,前提是 CSV 文件中存在类型信息。
- CSV 文件内容的修改不会在运行时反映到输出结果集中。
即使实现了类型安全,也是在运行时而不是编译时实现的。这是因为我们在运行时获取输入数据(CSV 文件)。
结论与未来展望
这是一个有趣的模块,它利用了 C# 语言最出色的特性之一 LINQ。该模块可以轻松扩展到 CSV 数据的实时显示、使用 LINQ 处理任何底层数据结构的能力、类型安全检查、运行时查询(从配置文件、用户输入等)。同样,IOutput
接口可以扩展以支持 html5 推送数据、文本输出、为厚客户端定制输出。
如果您计划在您的项目中使用此库(程序集),请给我发电子邮件。我将非常乐意为您提供此库的专业版本。另外,不要忘记为您喜欢的文章/项目投票。
历史
- 2019 年 2 月 3 日 - 版本 1
- 2019 年 2 月 16 日 - 添加了
RuntimeType
安全检查 - 版本 1.1