当我们有一个大型 C# 项目时,我们很快就会开始处理大量数据。这些数据可以来自各种来源:它可以由开发人员手写,从数据库中提取,从在线 URL 检索......由于所有这些源可能具有不同的约定,因此您最终可能会得到一些非常疏远的数据格式。
现在,假设你想通读这个你在网上获取的国家列表,按大洲过滤;或者您想要获取 PostgreSQL 数据库中名称以 “Tom” 开头的所有产品;或者您想要获取对等节点编写的 static 变量来定义应用程序设置并检查这些设置是否为 null。
乍一看,这些任务似乎很难重新组合成类似的流程,令人沮丧。由于数据的形状不同,当然,不可能对其进行标准化的探索,对吧?
嗯,这正是你可以使用一个超级酷的 C# 工具的情况:Linq!那么,今天,让我们来看看这个系统,看看它如何帮助我们简化数据获取或将数据从一种格式转换为另一种格式:)
Linq 的全部意义在于为 C# 开发人员提供一种声明性查询语法,允许他们使用他们熟悉的对象(类、方法、事件等)来解析和搜索数据。
注意:尽管 Linq 查询可以很好地处理编译时类型推断,但它们是基于泛型类型的 — 如果您想了解有关这些类型的更多信息,请查看我关于 C# 中的泛型的其他文章之一 ;)
除了使获取和过滤更容易之外,Linq 对于从一种语言到另一种语言的数据转换也非常有用。多亏了这个中间的 “normalizer”,您可以从 SQL 数据库中获取一些信息并将其写入 XML 文件,所有这些都在一个查询中完成!你也可以轻松地重新映射一些数据,这意味着你可以将初始数据转换为一组新的数据,一次一项(这里我说的是像 map/filter/reduce 范式这样的映射,例如在 JavaScript 中非常常见的)。
它还可以帮助将多个源合并到一个输出中,例如联接来自多个 SQL 表或多个 C# 可枚举项的结果,如下所示。
由于 C# Linq 实现与 C# 语言的其余部分一样是强类型的,因此您可以在大多数 IDE 中获得类型检查和 IntelliSense — 这真的很棒,无需深入研究大量表架构即可查看某些集合!
好了,是时候深入研究使用 Linq 的实际 C# 程序了!在 C# 中,Linq 可以通过两种方式使用:查询语法或方法语法。
假设您正在为一家杂货店清点库存。你有一个像这样的基本结构:Product
public struct Product
{
public string Name { get; init; }
public int CurrentAmount { get; init; }
public int MaxAmount { get; init; }
}
您有一些数据(在实际场景中,它可能存储在数据库中;在这里,我保持简单并将它们放在 C# 列表中):
List<Product> products = new List<Product>()
{
new Product { Name = "Apple", CurrentAmount = 2, MaxAmount = 3 },
new Product { Name = "Banana", CurrentAmount = 4, MaxAmount = 8 },
new Product { Name = "Pear", CurrentAmount = 2, MaxAmount = 2 },
new Product { Name = "Peach", CurrentAmount = 1, MaxAmount = 10 },
new Product { Name = "Tomato", CurrentAmount = 4, MaxAmount = 4 },
new Product { Name = "Raspberry", CurrentAmount = 0, MaxAmount = 4 },
new Product { Name = "Blueberry", CurrentAmount = 0, MaxAmount = 4 },
};
然后,多亏了 Linq,你可以非常轻松地对你的数据进行一些过滤。想要在您的商店网页上模拟搜索输入,并获取名称以“Pe”开头的所有产品?没问题!
class Program
{
static void Main(string[] args)
{
List<Product> products = new List<Product>() { ... }; IEnumerable<Product> queryByName =
from product in products
where product.Name.StartsWith("Pe")
select product;
foreach (Product p in queryByName) {
Console.WriteLine("\"{0}\" matches the query!", p.Name);
}
// output:
// "Pear" matches the query!
// "Peach" matches the query!
}
}
或者,也许您想检查一下您是否没有某些产品,以便您可以告诉您的客户等待一段时间?
class Program
{
static void Main(string[] args)
{
List<Product> products = new List<Product>() { ... }; List<string> outOfStock =
(from product in products
where product.CurrentAmount == 0
select product.Name).ToList();
Console.WriteLine("You're out of: {0}", String.Join(", ", outOfStock));
// output:
// You're out of: Raspberry, Blueberry
}
}
您还可以显示每种产品库存的“成交量”,即当前金额与最大金额的比率。为此,让我们切换并使用方法语法进行更改:
class Program
{
static void Main(string[] args)
{
var ratios = products
.Select((Product p) => new KeyValuePair<string, float>(
p.Name, p.CurrentAmount / (float) p.MaxAmount
));
Console.WriteLine("Fill amounts:");
foreach (var ratio in ratios) {
Console.WriteLine("{0}: {1}%", ratio.Key, ratio.Value * 100);
}
// output:
//
// Fill amounts:
// Apple: 66.66667%
// Banana: 50%
// Pear: 100%
// Peach: 10%
// Tomato: 100%
// Raspberry: 0%
// Blueberry: 0%
}
}
在这里,我还使用了 C# 关键字来隐式键入我的变量,因为它使编写 ;) 的时间非常短var
最后,假设您要解析当前数据以获取库存较少(剩余产品少于 2 个)的产品,并将其导出为 XML 文件以发送给您的同事。您可以使用 Linq 及其 类来过滤数据_并_编写 XML 树:XElement
class Program
{
static void Main(string[] args)
{
List<Product> products = new List<Product>() { ... }; XElement xmlTree = new XElement("ProductsRunningLow",
from product in products
where product.CurrentAmount <= 2
select new XElement(
"Product",
product.Name,
new XAttribute("amount", product.CurrentAmount.ToString())
)
);
Console.WriteLine(xmlTree);
// output:
//
// <ProductsRunningLow>
// <Product amount="2">Apple</Product>
// <Product amount="2">Pear</Product>
// <Product amount="1">Peach</Product>
// <Product amount="0">Raspberry</Product>
// <Product amount="0">Blueberry</Product>
// </ProductsRunningLow>
}
}
如你所见,末尾的部分允许你专门选择你想要的返回项的部分 - 你可以返回整个项,只返回一个字段,甚至是一个复杂的对象,就像我们在这里实例化的那样。selectXElement
注意: Linq 适用于任何 ,因此你甚至可以遍历 ed 元素!有关_示例,请查看 Microsoft C# 文档。IEnumeratoryield_
我们已经讨论了 Linq 的两种可能语法:查询语法和方法语法。请记住,这两者对于程序来说是绝对等效的,即使查询语法通常编写速度更快且可读性更强。但在少数情况下,只有方法调用可用,例如 or !Count()Max()
此外,Linq 需要注意的重要一点是,它默认处理延迟执行;换句话说,如果我在第一个查询中使用 basic like 而不是将其转换为 list,则在我到达循环之前不会执行查询。这在内存管理和执行时间方面非常强大,因为它可以避免在大查询时出现一些瓶颈 - 通过将这些瓶颈推迟到更合适的时间,您可以巧妙地将这个大查询时间“隐藏”给最终用户。IEnumerableforeach
但是,一旦你把 u 转换到一个列表或数组,或者如果你应用一个范围操作(比如 , , ...),就会立即强制执行,因为程序需要遍历整个集合才能得到结果。IEnumerableFirst()Average()Min()
请注意,强制转换到列表或数组允许您缓存结果;如果你不这样做,那么每次你遍历 Iterable 时都会重新运行查询,这通常没有优化......
Linq 是一个非常值得了解的工具,在处理 C# 数据时,绝对值得将其牢记在心。
但是,Linq 并不总是完美的选择。
特别是,如果您不小心,您很快就会陷入一个非常大且棘手的查询中。而且,由于查询通常是延迟和流式的,因此在发生异常时可能真的很难进行调试(因为,基本上,您的 IDE 会为您提供一个与查询的任何部分都不匹配的错误行!
与手动编写数据检索和转换管道相比,您可能还会不时缺少一些运算符 — 通常,有些 SQL 操作无法使用 Linq 完成。
此外,在某些情况下,Linq 可能会带来一些查询开销,如果您在应用程序的关键路径中使用 Linq,这可能会减慢您的程序速度——尤其是在您处理大型数据集时。
因此,为了确保您充分利用它,请记住进行一些测试,并每隔一段时间将运行查询所需的时间与简单的 for 循环进行比较;)
总而言之,Linq 是 C# 工具箱的一个很好的补充,它可以使代码更具可读性,或帮助数据重新规范化。但与任何工具一样,您应该始终小心不要过度使用它,并练习正确使用它......
(是的:如果您正在开发性能关键型应用程序,请不要忘记将 Linq 查询与基本循环进行基准测试!
当然,本文只是对这个工具的非常简短的介绍——我只展示了这项技术所能实现的一小部分。因此,如果您有兴趣,请务必更深入地了解 C# 中的 Linq 以及它如何连接到其他数据格式。