【发布时间】:2013-02-19 02:43:31
【问题描述】:
Linq 是对 .NET 的一个很棒的补充,我发现它在许多情况下都能很好地为我服务,即使我才刚刚开始学习如何使用 Linq。
但是,在阅读有关 Linq 的文章时,我发现开发人员需要注意一些微妙的事情,以免导致麻烦。
我已经包含了一个明确的警告,这是我遇到的延迟执行的结果。
所以我想知道,Linq 的新开发人员还应该知道哪些其他警告?
【问题讨论】:
Linq 是对 .NET 的一个很棒的补充,我发现它在许多情况下都能很好地为我服务,即使我才刚刚开始学习如何使用 Linq。
但是,在阅读有关 Linq 的文章时,我发现开发人员需要注意一些微妙的事情,以免导致麻烦。
我已经包含了一个明确的警告,这是我遇到的延迟执行的结果。
所以我想知道,Linq 的新开发人员还应该知道哪些其他警告?
【问题讨论】:
在 foreach 循环中构建查询
IEnumerable<char> query = "Not what you might expect";
foreach(char vowel in "aeiou")
{
query = query.Where(c => c != vowel);
}
由于延迟执行,上面的代码只删除了字符串中的“u”。
要删除所有元音,您需要执行以下操作:
IEnumerable<char> query = "Not what you might expect";
foreach(char vowel in "aeiou")
{
char temp = vowel;
query = query.Where(c => c != temp);
}
【讨论】:
我认为 LINQ 相当可靠,并且没有太多需要注意的地方。我遇到的几乎每一个“问题”都是延迟执行的结果,这并不是真正的问题,而是一种不同的思维方式。
我遇到的最大问题 - 在性能分析方面,LINQ 改变了游戏规则(或者至少是规则扭曲者)。延迟执行有时会使分析应用程序变得更加困难,并且还会以意想不到的方式显着改变运行时性能特征。某些 LINQ 操作的速度似乎很神奇,而其他操作所需的时间比我预期的要长得多 - 但从代码或分析器结果中并不总是很明显。
话虽如此,一般而言,延迟执行足以弥补它减慢手动编码例程的情况。我更喜欢更简单、更干净的代码,而不是它替换的代码。
另外,我发现我使用 LINQ to Objects 的次数越多,我就越需要重新思考我的设计并重新设计我的集合。
例如,在我开始频繁使用 linq to objects 之前,我从来没有意识到我暴露 IList 而不是 IEnumerable 的频率是多少。我现在完全理解为什么 MS 设计指南警告不要经常使用 IList(例如,不要只为 Count 属性返回 IList 等)。当我有采用 IList 的方法时,传递来自 linq 查询的 IEnumerable 结果需要 .ToList() 或对该方法的 API 进行改造。
但这几乎总是值得重新思考 - 我发现在许多地方传递可枚举和使用 LINQ 会产生巨大的性能。收益。如果您考虑一下并充分利用它,延迟执行就很棒。例如,使用 .Take() 将集合限制为前 2 个元素(如果这就是所有需要的话)在 pre-linq 之前更具挑战性,并且大大加快了我的一些更糟糕的循环。
【讨论】:
好问题。正如 Reed 指出的那样,它们大多来自deferred execution(但与他不同的是,我发现这是一个缺点。只是在想为什么不能通过记忆状态来执行延期执行)。这里有几个例子 - 或多或少都是延迟执行问题的变体。
1) 我懒得按时做事
Linq 仅按需执行。
新手(包括过去的我自己)常犯的一个错误是不知道延迟执行。例如,像
var p = listOfAMillionComplexItems.OrderBy(x => x.ComplexProperty);
运行时间很短,但实际排序直到您枚举列表时才完成,换句话说,直到您需要执行结果时才完成执行。要执行它,您需要以下内容:
foreach(var item in p)...
//or
p.Count();
//or
p.ToList();
//etc
将它们视为 SQL 查询。如果你有
var query = from i in otherValues where i > 5 select i;
认为它类似于写作
string query = "SELECT i FROM otherValues WHERE i > 5";
后者是否运行对 db 的调用?不,你必须
Execute(query);
Linq 也是如此。
2) 我活在当下
小心 Linq 表达式中的变量发生变化 稍后。
为了安全起见,先备份变量,然后在查询中使用备份,如果变量可以在实际执行查询之前更改。
decimal minimumBalance = 500;
var customersOver500 = from c in customers
where c.Balance > minimumBalance
select c;
minimumBalance = 200;
var customersOver200 = from c in customers
where c.Balance > minimumBalance
select c;
int count1 = customersOver500.Count();
int count2 = customersOver200.Count();
假设我们有四个客户,其余额如下:100、300、400 和 600。count1 和 count2 将是什么?它们都是 3。“customersOver500”引用了“minimumBalance”变量,但直到迭代查询结果(通过 for/each 循环、ToList() 调用甚至是“Count ()" 调用如上所示)。在使用该值处理查询时,minimumBalance 的值已更改为 200,因此两个 LINQ 查询产生相同的结果(余额超过 200 的客户)。
3) 我的记忆力太弱,记不住过去的贵重物品
同上,上下文略有不同。
或者来自同一个站点:
考虑这个使用 LINQ-to-SQL 获取客户列表的简单方法示例:
public IEnumerable<Customer> GetCustomers()
{
using(var context = new DBContext())
{
return from c in context.Customers
where c.Balance > 2000
select c;
}
}
看起来很无害——直到你在尝试枚举集合时得到“ObjectDisposedException”。为什么?因为 LINQ 在您尝试枚举结果之前实际上不会执行查询。 DBContext 类(它公开了 Customers 集合)在此调用退出时被释放。一旦您尝试枚举该集合,就会引用 DBContext.Customers 类并获得异常。
4) 别想抓住我,我可能还是会溜走
如果不明智地使用,Try-catch 对语句毫无意义。
相反,全局异常处理在这里会更好。
try
{
wallet = bank.Select(c => Convert.ToInt32(""));
}
catch (Exception ex)
{
MessageBox.Show("Cannot convert bad int");
return;
}
foreach(int i in wallet)
//kaboom!
return 既没有得到正确的错误消息,也没有退出函数。
5) 我不仅不守时,而且我也没有从错误中吸取教训
每次枚举它们时都会执行 Linq。所以不要重用 Linq 枚举。
假设您有一个从 Linq 表达式返回的 IQueryable 或 IEnumerable。现在枚举集合将执行语句,但只执行一次?不,每次你这样做。这在过去曾咬过我。如果你有:
var p = listOfAMillionComplexItems.OrderBy(x => x.ComplexProperty);
MessageBox.Show(p.Count().ToString()); //long process.
MessageBox.Show(p.Count().ToString()); //long process still.
那就更好了
int i = p.Count(); //store in a variable to access count
//or better
var list = p.ToList(); //and start using list
6) 如果你不知道用我,我会造成副作用!
与上面相同,只是为了说明重用 Linq 枚举如何导致不良行为。
确保你不进行副作用编程(因为在 Linq 中重新枚举更为常见)举个狂野的例子,
p = bag.Select((t, i) => {if(i == 1) MessageBox.Show("Started off"); return t;});
如果你列举两次,你就会知道会发生什么不希望发生的事情。
7) 链接时要注意我的执行顺序
不仅对于变量,即使是链接的 Linq 函数也可以按照与您通常期望的顺序不同的顺序执行(尽管行为是正确的)。不要认为势在必行(一步一步),想想 Linq 怎么可能执行它。
例如,
var d = Enumerable.Range(1, 100);
var f = d.Select(t => new Person());
f = f.Concat(f);
f.Distinct().Count() ??
f 中不同的人的数量是多少?我猜是 100,但它是 200。问题是当实际执行连接逻辑时,f 仍然是d.Select(t => new Person() 未执行。所以这有效地产生了
f = d.Select(t => new Person()).Concat(d.Select(t => new Person()));
然后有 200 个不同的成员。 Here's a link for the actual problem
8) 嘿,实际上我们比你想象的要聪明。
本身并不是一个警告,但在很多情况下,Linq 可以胜过您的命令式程序。因此,在优化之前,请三思而后行,甚至进行基准测试。
延迟执行基本上是按需执行的原因,使得 Linq 比看起来要高效得多。迭代器块根据需要一次“生成”一个项目,从而在不再需要时停止执行。这是一个很好的问题,详细说明了这一点:Order of LINQ extension methods does not affect performance?
9) 我不是要计算数字
滥用 Linq 会使代码效率低下且可读性降低。
对于数字运算算法,Linq 不是正确的工具,尤其是对于复杂性可以呈指数级扩展的大型数据集。有时只有两个 for 循环就足够了。与 LINQ to SQL 相比,这同样适用于原始 SQL。
10) 雇用我做合适的工作
让 Linq 关注你的正常业务是糟糕的编程选择,这违背了可读性。
一些例如:
medicines.Any(p =>
{
Console.WriteLine(p);
return false;
});
对于一个可枚举的 foreach。
或
medicines = medicines.Select(p =>
{
p.Id = 3;
return p;
});
只是糟糕的工具。
11) 调试和分析可能是一场噩梦
很难从 VS 中了解 Linq 表达式背后发生的事情
并不是说它完全不可能,但它是一项任务,可以像 VS 本身的非 linq 代码一样高效地调试 linq 查询。由于延迟执行的性质,分析也变得有点困难。但这不应该阻止任何人做微不足道的一两个班轮!
一堆或多或少与延迟执行有关的警告! A ditto question here。关于 SO 的一些相关阅读:
Examples on when not to use LINQ
Pros and Cons of LINQ (Language-Integrated Query)
What is the biggest mistake people make when starting to use LINQ?
【讨论】: