再次延迟执行:LINQ 中 Random 的本地实例 query/queries
Deferred execution again: Local instance of Random inside LINQ query/queries
我想了解我今天偶然发现的以下行为。这个小程序展示了 "problem":
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
说 "sequence does not contain a matching element".
但是,一旦我预先计算出随机整数,它就会按预期工作:
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
替换它。)
问题是您每次评估谓词时都针对不同的 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) } );
请注意,由于 bars
的惰性求值,您现在可能会得到多个具有相同 ID 的 Bar
实例 - 您可能希望在 ToList()
结束时调用分配给 bars
.
我想了解我今天偶然发现的以下行为。这个小程序展示了 "problem":
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
说 "sequence does not contain a matching element".
但是,一旦我预先计算出随机整数,它就会按预期工作:
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
替换它。)
问题是您每次评估谓词时都针对不同的 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) } );
请注意,由于 bars
的惰性求值,您现在可能会得到多个具有相同 ID 的 Bar
实例 - 您可能希望在 ToList()
结束时调用分配给 bars
.