Linq运行总计第一个值加到自身

时间:2014-07-30 12:37:56

标签: c# linq

我有以下计算客户帐户状态的运行总计,但是他的第一个值总是添加到自身,我不知道为什么 - 虽然我怀疑我错过了一些明显的东西:

    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()附加到我的查询。

感谢大家的建议

2 个答案:

答案 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将被重置。