有趣的是,有些人选择IEnumerable<T>,而有些人却坚持使用IReadOnlyList<T>。
现在说实话。 IEnumerable<T> 很有用,很有用。在大多数情况下,您只想将此方法放在某个库中,然后将您的实用程序函数扔给您认为是集合的任何内容,然后完成它。但是,正确使用IEnumerable<T> 有点棘手,我将在此处指出...
IEnumerable
让我们假设 OP 正在使用 Linq 并希望从序列中获取随机元素。基本上,他最终得到了来自 @Yannick 的代码,这些代码最终出现在实用程序帮助函数库中:
public static T AnyOne<T>(this IEnumerable<T> source)
{
int endExclusive = source.Count(); // #1
int randomIndex = Random.Range(0, endExclusive);
return source.ElementAt(randomIndex); // #2
}
现在,这基本上做了两件事:
- 计算源中元素的数量。如果源是简单的
IEnumerable<T>,这意味着遍历列表中的所有元素,如果它是 f.ex。 List<T>,它将使用 Count 属性。
- 重置可枚举,转到元素
randomIndex,抓住它并返回它。
这里有两件事可能出错。首先,您的 IEnumerable 可能是一个缓慢的顺序存储,并且执行Count 可能会以意想不到的方式破坏您的应用程序的性能。例如,从设备流式传输可能会给您带来麻烦。话虽如此,您很可能会争辩说,当这是该系列的固有特征时,这是可以预料的——而且我个人认为这个论点会成立。
其次 - 这可能更重要 - 不能保证您的 enumerable 每次迭代都会返回相同的序列(因此也不能保证您的代码不会崩溃)。例如,考虑一下这段看似无害的代码,它可能对测试有用:
IEnumerable<int> GenerateRandomDataset()
{
Random rnd = new Random();
int count = rnd.Next(10, 100); // randomize number of elements
for (int i=0; i<count; ++i)
{
yield return new rnd.Next(0, 1000000); // randomize result
}
}
第一次迭代(调用Count()),您可能会生成 99 个结果。您选择元素 98。接下来您调用 ElementAt,第二次迭代生成 12 个结果并且您的应用程序崩溃。不酷。
修复 IEnumerable 实现
正如我们所见,IEnumerable<T> 实现的问题是您必须遍历数据 2 次。我们可以通过一次检查数据来解决这个问题。
这里的“技巧”实际上非常简单:如果我们看到了 1 个元素,我们肯定要考虑返回它。考虑到所有元素,有 50%/50% 的机会这是我们会返回的元素。如果我们看到第三个元素,我们将有 33%/33%/33% 的机会返回它。以此类推。
因此,一个更好的实现可能是这个:
public static T AnyOne<T>(this IEnumerable<T> source)
{
Random rnd = new Random();
double count = 1;
T result = default(T);
foreach (var element in source)
{
if (rnd.NextDouble() <= (1.0 / count))
{
result = element;
}
++count;
}
return result;
}
附带说明:如果我们使用 Linq,我们希望操作使用 IEnumerable<T> 一次(而且只有一次!)。现在你知道为什么了。
使其适用于列表和数组
虽然这是一个巧妙的技巧,但如果我们在 List<T> 上工作,我们的性能现在会变慢,这没有任何意义,因为我们知道,由于 indexing 和 @987654339 的属性,有更好的实现可用@ 可供我们使用。
我们正在寻找的是这个更好的解决方案的共同点,我们可以在尽可能多的集合中使用它。我们最终得到的是IReadOnlyList<T> 接口,它实现了我们需要的一切。
由于我们知道适用于IReadOnlyList<T> 的属性,我们现在可以安全地使用Count 和索引,而不会冒应用程序崩溃的风险。
然而,虽然IReadOnlyList<T> 看起来很吸引人,但IList<T> 出于某种原因似乎并没有实现它......这基本上意味着IReadOnlyList<T> 在实践中有点像赌博。在这方面,我很确定IList<T> 的实现比IReadOnlyList<T> 的实现要多得多。因此,最好只支持这两个接口。
这将我们引向这里的解决方案:
public static T AnyOne<T>(this IEnumerable<T> source)
{
var rnd = new Random();
var list = source as IReadOnlyList<T>;
if (list != null)
{
int index = rnd.Next(0, list.Count);
return list[index];
}
var list2 = source as IList<T>;
if (list2 != null)
{
int index = rnd.Next(0, list2.Count);
return list2[index];
}
else
{
double count = 1;
T result = default(T);
foreach (var element in source)
{
if (rnd.NextDouble() <= (1.0 / count))
{
result = element;
}
++count;
}
return result;
}
}
PS:对于更复杂的场景,请查看策略模式。
随机
@Yannick Motton 说你必须小心Random,因为如果你多次调用这样的方法,它就不会是真正随机的。 Random 是用 RTC 初始化的,所以如果你多次创建一个新实例,它不会改变种子。
一个简单的解决方法如下:
private static int seed = 12873; // some number or a timestamp.
// ...
// initialize random number generator:
Random rnd = new Random(Interlocked.Increment(ref seed));
这样,每次调用 AnyOne 时,随机数生成器都会收到另一个种子,即使在紧密的循环中也能正常工作。
总结一下:
所以,总结一下:
-
IEnumerable<T> 应该被迭代一次,并且只被迭代一次。否则可能会给用户带来意想不到的结果。
- 如果您可以获得比简单枚举更好的功能,则无需遍历所有元素。最好立即获得正确的结果。
- 仔细考虑您正在检查的接口。虽然
IReadOnlyList<T> 绝对是最佳候选,但它不是从IList<T> 继承的,这意味着它在实践中的效率会降低。
最终结果是 Just Works。