Linq运行总第一个值添加到自己
本文关键字:添加 自己 第一个 运行 Linq | 更新日期: 2023-09-27 18:15:31
我有以下计算客户帐户状态的运行总数,但是第一个值总是添加到自己,我不确定为什么-尽管我怀疑我错过了一些明显的东西:
decimal? runningTotal = 0;
IEnumerable<StatementModel> statement = sage.Repository<FDSSLTransactionHistory>()
.Queryable()
.Where(x => x.CustomerAccountNumber == sageAccount)
.OrderBy(x=>x.UniqueReferenceNumber)
.AsEnumerable()
.Select(x => new StatementModel()
{
SLAccountId = x.CustomerAccountNumber,
TransactionReference = x.TransactionReference,
SecondReference = x.SecondReference,
Currency = x.CurrencyCode,
Value = x.GoodsValueInAccountCurrency,
TransactionDate = x.TransactionDate,
TransactionType = x.TransactionType,
TransactionDescription = x.TransactionTypeName,
Status = x.Status,
RunningTotal = (runningTotal += x.GoodsValueInAccountCurrency)
});
输出:29/02/2012 00:00:00 154.80 309.60
30/04/2012 00:00:00 242.40 552.00
30/04/2012 00:00:00 242.40 794.40
30/04/2012 00:00:00 117.60 912.00
第一行的309.60
应该是简单的154.80
我做错了什么?
编辑:根据ahruss下面的评论,我在我的视图中调用Any()
的结果,导致第一个被评估两次-为了解决我将ToList()
附加到我的查询。
谢谢大家的建议
在调用的末尾添加一个ToList()
,以避免重复调用选择器
这是一个具有副作用的有状态LINQ查询,其本质上是不可预测的。在代码的其他地方,您调用了导致第一个元素被求值的东西,如First()
或Any()
。一般来说,在LINQ查询中产生副作用是很危险的,当你发现自己需要它们时,是时候考虑是否应该只使用foreach
了。
编辑,或者为什么会发生这种情况?
这是LINQ查询计算方式的结果:在您实际使用查询结果之前,集合实际上没有发生任何变化。它不计算任何元素的值。相反,它存储抽象表达式树或仅存储计算查询所需的委托。然后,它只在需要结果时才计算这些结果,除非您显式地存储结果,否则它们将在之后被丢弃,并在下次重新计算。
那么问题来了,为什么每次都有不同的结果?答案是runningTotal
只在第一次初始化。在此之后,它的值是最后一次执行查询后的值,这可能导致奇怪的结果。
这意味着问题可以很容易地变成"为什么总数总是应该是它的两倍?"如果提问者像这样做:
Console.WriteLine(statement.Count()); // this enumerates all the elements!
foreach (var item in statement) { Console.WriteLine(item.Total); }
因为获得序列中元素个数的唯一方法是实际计算所有元素的值
同样,在这个问题中实际发生的情况是,在某个地方有这样的代码:
if (statement.Any()) // this actually involves getting the first result
{
// do something with the statement
}
// ...
foreach (var item in statement) { Console.WriteLine(item.Total); }
这似乎是无害的,但如果你知道LINQ和IEnumerable是如何工作的,你就会知道.Any()
基本上和.GetEnumerator().MoveNext()
是一样的,这使得它更明显地需要获得第一个元素。
这一切都归结为LINQ是基于延迟执行的事实,这就是为什么解决方案是使用ToList
,它绕过了这个问题并强制立即执行。
如果您不想使用ToList
冻结结果,那么解决外部作用域变量问题的方法是使用迭代器函数,如下所示:
IEnumerable<StatementModel> GetStatement(IEnumerable<DataObject> source) {
decimal runningTotal = 0;
foreach (var x in source) {
yield return new StatementModel() {
...
RunningTotal = (runningTotal += x.GoodsValueInAccountCurrency)
};
}
}
然后将源查询(不包括Select
)传递给该函数:
var statement = GetStatement(sage.Repository...AsEnumerable());
现在可以安全地枚举statement
多次了。基本上,这创建了一个可枚举对象,在每个枚举上重新执行整个块,而不是执行选择器(它只相当于foreach
部分)——因此runningTotal
将被重置。