再次延迟执行:LINQ查询/查询中的Random的本地实例

时间:2015-09-01 14:54:15

标签: c# linq deferred-execution

我希望了解今天偶然发现的以下行为。这个小程序展示了"问题":

class Bar
{
  public int ID { get; set; }
}

class Foo
{
  public Bar Bar { get; set; }
}

class Program
{
  private static IEnumerable<Bar> bars;
  private static IEnumerable<Foo> foos;

  static void Main(string[] args)
  {
    Random rng = new Random();
    bars = Enumerable.Range(1, 5).Select(i => new Bar { ID = i });
    foos = Enumerable.Range(1, 10).Select(i => new Foo
    {
      Bar = bars.First(b => b.ID == rng.Next(5) + 1)
    });
    var result = foos.ToList();
  }
}

此代码不起作用;它抛出一个InvalidOperationException说&#34;序列不包含匹配的元素&#34;。

但是,一旦我事先计算出随机整数,它就会按预期工作:

var f = new List<Foo>();
for (int i = 0; i <= 20; i++)
{
  int r = rng.Next(5) + 1;
  f.Add(new Foo
  {
    Bar = bars.First(b => b.ID == r)
  });
}
foos = f;

正如标题所述,我怀疑延迟执行是其原因。但是,如果有人能够通过第一个代码查明问题并解释其背后的确切原因,我感到很高兴。

(轶事:rng.Next(1) + 1 工作,但我想编译器足够聪明,可以用常量1替换它。)

1 个答案:

答案 0 :(得分:2)

问题在于,每次评估谓词时都要对不同的ID进行测试。

您根本不需要foos。你可以拥有:

var bar = bars.First(b => b.ID == rng.Next(5) + 1);

这将最多执行5次谓词 - 每个元素bar一次 - 生成一个新的ID,以便每次都进行测试。换句话说,它是这样的:

public Bar FindRandomBarBroken(IEnumerable<Bar> bars, Random rng)
{
    foreach (var bar in bars)
    {
        if (bar.ID == rng.Next(bar.Count) + 1)
        {
            return bar;
        }
    }
    throw new Exception("I didn't get lucky");
}

你需要类似的东西:

public Bar FindRandomBarFixed(IEnumerable<Bar> bars, Random rng)
{
    // Only generate a single number!
    int targetId = rng.Next(bar.Count) + 1;
    foreach (var bar in bars)
    {
        if (bar.ID == targetId)
        {
            return bar;
        }
    }
    throw new Exception("Odd - expected there to be a match...");
}

您可以使用其他Select调用来解决此问题,以生成随机ID:

foos = Enumerable.Range(1, 10)
                 .Select(_ => rng.Next(5) + 1)
                 .Select(id => new Foo { Bar = bars.First(b => b.ID == id) } );

请注意,由于Bar的延迟评估,您现在可能会获得具有相同ID的多个bars个实例 - 您可能希望在作业结束时进行ToList()调用到bars