我有以下计算客户帐户状态的运行总计,但是他的第一个值总是添加到自身,我不知道为什么 - 虽然我怀疑我错过了一些明显的东西:
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()
附加到我的查询。
感谢大家的建议
答案 0 :(得分:7)
在调用结束时添加ToList()
以避免重复调用选择器。
这是一个带有副作用的有状态LINQ查询,这本质上是不可预测的。在代码的其他地方,您调用了导致第一个元素被评估的内容,例如First()
或Any()
。一般来说,在LINQ查询中产生副作用是危险的,当你发现自己需要它们时,是时候考虑它是否应该只是foreach
。
这是对LINQ查询进行评估的结果:在您实际使用查询结果之前,集合中没有任何实际情况发生。它不评估任何元素。相反,它存储Abstract Expression Trees或仅存储评估查询所需的委托。然后,它仅在需要结果时评估这些结果,除非您明确存储结果,否则它们会在之后被丢弃,并在下次重新评估。
所以这就是为什么每次都有不同的结果?答案是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()
基本相同,这使得它更需要获得第一个元素。< / p>
这一切都归结为LINQ基于延迟执行的事实,这就是为什么解决方案是使用ToList
,这会绕过它并强制立即执行。
答案 1 :(得分:1)
如果您不希望使用ToList
冻结结果,则外部范围变量问题的解决方案是使用iterator function,如下所示:
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
将被重置。