Linq选择投影与计算值

3

我注意到当使用具有匿名函数或在运行时计算的其他值的Select时,每次访问输出IEnumerable对象时,投影都会重新计算该值。例如:

public class A
{
    public string Name { get; set; }
    public string Addr { get; set; }
}
public class B
{
    public A Whatever {get;set;}
    public int Id {get;set;}
}

Random rand = new Random();
IEnumerable<B> listOfBs = someListOfA.Select( x => new B()
{
    Id = rand.Next(),
    Whatever = x
});

我注意到每次遍历listOfB并使用Id属性时,Id都会重新计算出一个随机数rand.Next()

foreach( B b in listOfBs )
{
  doSomething( b.Id );
}

在C#的文档中,我没有看到有什么东西会导致这种情况。几乎可以说Select生成了一个匿名函数,每次访问时都会重新评估。所以,两个问题:

  1. 我看到的这种行为是什么。
  2. 如何避免这种行为,但仍然能够将一个类型的列表转换为另一个类型的列表。

如果我的糟糕示例代码传达了我的意思,请告诉我。


你尝试运行这个示例代码了吗?在迭代listOfBs时,它每次都有不同的随机值吗? - Chetan
var someListOfA = new List<A>() { new A() }; ... foreach (var b in listOfBs) { Console.WriteLine(b.Id); } foreach (var b in listOfBs) { Console.WriteLine(b.Id); } console: "756542965 1936550407",这展示了 OP 所遇到的问题。 - Blake Thingstad
这是预期的,在执行查询之前,Linq已经进行了延迟执行。如果你执行listOfB = listOfB.ToList(),查询将被执行... - Johnny
是的,我也看到了,调用ToList()解决了这个“问题”。 - MonkeyWrench
1个回答

4
预计会出现这种情况,因为大多数LINQ方法都是惰性的 - 这意味着它们不会枚举源,直到需要结果为止。在您的特定场景中,listOfBs 实际上并不是一个实体化的 B 对象集合。相反,它是一个关于如何将 someListOfA 转换为 B 对象集合的定义。Select 返回一个实现了 IEnumerable<T> 接口的对象,并存储对源集合和投影委托的引用。当需要结果时进行投影,例如在 foreach 中遍历集合时。如果您多次迭代,则会执行多次投影。这正是您看到的情况。
调用 ToListToArray 立即实体化结果:
IEnumerable<B> listOfBs = someListOfA.Select( x => new B()
{
    Id = rand.Next(),
    Whatever = x
}).ToList();

谢谢。有道理。如果你没有预料到它,也很危险。Linq允许您轻松编写破损的代码,如果不小心,就像可能或可能不使用相同线程的异步编程模型一样。语法糖可以捕捉不谨慎的人。 - MonkeyWrench
我猜另一个问题是,在已经解析了整个IEnumerable元素列表之后调用ToList()方法的效率如何,它会多次运行列表吗? - MonkeyWrench
source.Select().ToList 只会枚举一次源集合。 - MarcinJuraszek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接