为什么 LINQ 会根据我放置 ToList 的位置而执行相同的代码两次?

huangapple go评论67阅读模式
英文:

Why is linq executing the same code twice depending on where I put ToList?

问题

When you call the PayCost function with the Select method in the original code, it uses lazy evaluation, which means it defers execution until you actually enumerate the result. This leads to the function being called twice for each element in compositeAndCost.Costs.

To avoid this issue, you can use .ToList() to force immediate execution and store the results in a list. Here's the explanation for your two concrete questions:

  1. Mechanism for Calling the Function Twice:

    • Lazy Evaluation: LINQ operations like Select use lazy evaluation. They don't execute immediately but build a query plan. In your original code, the Select method constructs a query plan to call PayCost for each element but doesn't execute it until you iterate over the results.
  2. Avoiding This Kind of Bug in LINQ:

    • Use .ToList() or .ToArray(): If you want to ensure immediate execution and prevent multiple calls to a function, you can use .ToList() or .ToArray() as you did in your revised code. This materializes the query results into a list or array immediately.

By using .ToList() in your modified code, you ensure that the PayCost function is called only once for each element in compositeAndCost.Costs, resolving the issue caused by lazy evaluation.

英文:

When I call the following function:

public ICombatTransaction PayCost(CostTransaction costTransaction)
{
    if (costTransaction.Cost is CompositeAndCost compositeAndCost)
    {
        var res = compositeAndCost.Costs.Select(cost => PayCost(new CostTransaction(costTransaction.Source, cost, costTransaction, this)));

        if (res.All(t => t.IsFulfilled))
            return new CompositeFulfilledTransaction(res.ToList(), costTransaction, this);

        throw new Exception($"Composite 'and' system cannot pay ({string.Join(", ", res.Where(x => !x.IsFulfilled).Select(x => ((CostTransaction)x).Cost.GetType()))}) costs");
    }
    else
    {
        foreach (var costSystem in CostSystems)
            if (costSystem.Accepts(costTransaction.Cost.GetType()))
                return costSystem.PayCost(costTransaction);

        return costTransaction;
    }
}

The recursive PayCost call in line 5 ends up getting called twice for each element in
compositeAndCost.Costs. This is not intentional. Simply moving the ToList from under the if statement to above it resolves this problem entirely:

public ICombatTransaction PayCost(CostTransaction costTransaction)
{
    if (costTransaction.Cost is CompositeAndCost compositeAndCost)
    {
        var res = compositeAndCost.Costs.Select(cost => PayCost(new CostTransaction(costTransaction.Source, cost, costTransaction, this))).ToList();

        if (res.All(t => t.IsFulfilled))
            return new CompositeFulfilledTransaction(res, costTransaction, this);

        throw new Exception($"Composite 'and' system cannot pay ({string.Join(", ", res.Where(x => !x.IsFulfilled).Select(x => ((CostTransaction)x).Cost.GetType()))}) costs");
    }
    else
    {
        foreach (var costSystem in CostSystems)
            if (costSystem.Accepts(costTransaction.Cost.GetType()))
                return costSystem.PayCost(costTransaction);

        return costTransaction;
    }
}

This is one of the strangest C# bugs I've ever seen, I haven't seen anything like it in like 7 years of working with C# and Linq. I'm sure it's because of lazy evaluation (which I didn't know C# just did sometimes!) but I still don't understand the mechanism that leads to calling a function twice, and I don't understand why it isn't resolving to a list when res.All is called.

My concrete questions are 1. what's the exact mechanism that leads to calling the function twice and 2. what should I be thinking about/paying attention to in order to avoid this kind of bug when using linq?

答案1

得分: 1

我有同样的问题。
LinQ的Select返回一个IEnumerable。这意味着直到你需要访问数据时才会加载它。
例如,如果你有一个名为'MyDbContext'的数据库,其中有一个名为'Data'的表。
然后在某个方法中你使用它:

var records = MyDbContext.Data.Select(o => o.Id > 10); // 这里不会加载数据到records
var count = records.Count(); // 在这里,所有查询数据都从数据库加载并返回计数。
var secondCount = records.Count() // 在这里再次从数据库加载数据并返回计数。

但如果你将第一行更改为:

var records = MyDbContext.Data.Select(o => o.Id > 10).ToList();
那么数据将只加载一次,因为ToList()创建了一个包含数据的对象。
所以当你需要对同一数据进行多次操作,比如计数、排序等时,最好将它加载到列表或数组中。

英文:

I had same problem.
LinQ select return IEnumerable. It means, that it's not loading data until you need to access it.
For example if you have database 'MyDbContext' with table 'Data'.
Then in some method you use it:

var records = MyDbContext.Data.Select(o=>o.Id>10); // this not loading data to records
var count = records.Count(); // here all query data is loaded from DB and returns count.
var secondCount = records.Count() // here again data is loaded from DB and returns count. 

but if you change first line to:

var records = MyDbContext.Data.Select(o=>o.Id>10).ToList();

then data from database would be loaded once. Because ToList() creates an object with data.
So when you need to make several manipulations with same data, like counting, sorting and etc. it is better to load it to list or array.

huangapple
  • 本文由 发表于 2023年6月25日 20:05:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/76550301.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定