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

谢谢大家的建议

Linq运行总第一个值添加到自己

在调用的末尾添加一个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将被重置。