c#generic,包括数组和列表?

这是一个非常方便的扩展,适用于任何array

 public static T AnyOne(this T[] ra) where T:class { int k = ra.Length; int r = Random.Range(0,k); return ra[r]; } 

不幸的是,它不适用于List的任何东西。 这是适用于任何List的相同扩展名

 public static T AnyOne(this List listy) where T:class { int k = listy.Count; int r = Random.Range(0,k); return listy[r]; } 

事实上,是否有一种方法可以一次性推广包含arrayList s的generics? 还是知道不可能?


它发生在我身上,答案甚至可以进一步包含Collection s? 或者确实有下面的专家之一已经实现了??


PS,我很抱歉没有明确提到这是在Unity3D环境中。 “Random.Range”是一个统一到骨骼的function,“AnyOne”调用将100%的游戏引擎读作任何游戏工程师。 这是你为任何游戏项目输入的第一个扩展,并且你经常在游戏代码中使用它(“任何爆炸!”“任何硬币声音效果!”等等!)

显然,它当然可以在任何c#milieu中使用。

事实上,对于你的案例, T[]List之间最合适的公共接口是IReadOnlyList

 public static T AnyOne(this IReadOnlyList list) where T:class { int k = list.Count; int r = Random.Range(0,k); return list[r]; } 

正如在另一个答案中提到的, IList也可以工作,但是好的做法要求您从调用者请求该方法所需的最小function,在本例中是Count属性和只读索引器。

IEnumerable也可以工作,但它允许调用者传递一个非集合迭代器,其中CountElementAt扩展方法效率非常低 – 如Enumerable.Range(0, 1000000) ,数据库查询等。


Unity3D工程师的注意事项:如果你查看IReadOnlyList接口文档的最底层,它可以从.Net 4.5开始使用。 在早期版本的.Net中,您必须使用IList (自2.0起可用)。 Unity在.Net版本上远远落后。 2016年,Unity仅使用.Net 2.0.5。 因此,对于Unity3D,您必须使用IList

T[]List实际上都实现了IList ,它提供了枚举,Count属性和索引器。

 public static T AnyOne(this IList ra) { int k = ra.Count; int r = Random.Range(0,k); return ra[r]; } 

请注意:对于Unity3D环境,具体来说,这是正确的答案。 关于这个答案的进一步改进, IReadOnlyList ,它在Unity3D中不可用。 (关于IEnumerable的(巧妙)扩展甚至覆盖没有计数/可索引性的对象的情况,当然在游戏引擎情况下将是一个独特的概念(例如AnyOneEvenInefficientlyAnyOneEvenFromUnsafeGroups )。)

有些人选择IEnumerable是有趣的,而其他人坚持使用IReadOnlyList

现在让我们说实话。 IEnumerable非常有用,非常有用。 在大多数情况下,您只想将此方法放在某个库中,并将实用程序函数抛出到您认为的集合中,并完成它。 但是,正确使用IEnumerable有点棘手,我在这里指出……

IEnumerable的

让我们假设OP使用Linq并希望从序列中获取随机元素。 基本上他最终得到了来自@Yannick的代码,最终出现在实用程序辅助函数库中:

 public static T AnyOne(this IEnumerable source) { int endExclusive = source.Count(); // #1 int randomIndex = Random.Range(0, endExclusive); return source.ElementAt(randomIndex); // #2 } 

现在,这基本上做的是两件事:

  1. 计算源中元素的数量。 如果源是一个简单的IEnumerable这意味着遍历列表中的所有元素,如果它是f.ex. List ,它将使用Count属性。
  2. 重置可枚举,转到元素randomIndex ,抓住并返回它。

这里有两件事可能会出错。 首先,你的IEnumerable可能是一个缓慢的顺序存储,而做Count会以一种意想不到的方式破坏你的应用程序的性能。 例如,从设备流式传输可能会让您遇到麻烦。 也就是说,你可以很好地争辩说,当这个系列的特征固有的时候会有所期待 – 而且我个人认为这个论点会成立。

其次 – 这可能更重要 – 不能保证你的枚举每次迭代都会返回相同的序列(因此也无法保证你的代码不会崩溃)。 例如,考虑一下看似无辜的代码片段,它可能对测试有用:

 IEnumerable GenerateRandomDataset() { Random rnd = new Random(); int count = rnd.Next(10, 100); // randomize number of elements for (int i=0; i 

第一次迭代(调用Count() ),您可能会生成99个结果。 你选择元素98.接下来你调用ElementAt ,第二次迭代生成12个结果,你的应用程序崩溃。 不酷。

修复IEnumerable实现

正如我们所看到的, IEnumerable实现的问题是你必须经历2次数据。 我们可以通过一次浏览数据来解决这个问题。

这里的'技巧'实际上非常简单:如果我们看过1个元素,我们肯定要考虑返回它。 考虑到所有因素,这是我们将返回的元素的50%/ 50%的可能性。 如果我们看到第三个元素,我们就会有33%/ 33%/ 33%的可能性。 等等。

因此,更好的实现可能是这个:

 public static T AnyOne(this IEnumerable 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一次(并且只使用一次!)。 现在你知道为什么了。

使其适用于列表和数组

虽然这是一个巧妙的技巧,但如果我们处理List ,我们的性能现在会变慢,这没有任何意义,因为我们知道有更好的实现可用,因为索引和Count可用于我们。

我们正在寻找的是这个更好的解决方案的共同点 ,我们可以在尽可能多的集合中使用它。 我们最终得到的是IReadOnlyList接口,它实现了我们需要的一切。

由于我们知道 IReadOnlyList的属性,我们现在可以安全地使用Count和索引,而不会冒着崩溃应用程序的风险。

然而,虽然IReadOnlyList看起来很吸引人,但由于某种原因, IList似乎没有实现它......这基本上意味着IReadOnlyList在实践中有点赌博。 在这方面,我非常确定有比IReadOnlyList实现更多的IList IReadOnlyList实现。 因此,最好只支持两种接口。

这引出了我们解决方案:

 public static T AnyOne(this IEnumerable source) { var rnd = new Random(); var list = source as IReadOnlyList; if (list != null) { int index = rnd.Next(0, list.Count); return list[index]; } var list2 = source as IList; 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 ,因为如果你多次调用这样的方法,它就不会是随机的。 使用RTC初始化Random,因此如果您多次创建一个新实例,它将不会更改种子。

一个简单的方法如下:

 private static int seed = 12873; // some number or a timestamp. // ... // initialize random number generator: Random rnd = new Random(Interlocked.Increment(ref seed)); 

这样,每次调用AnyOne时,随机数生成器都会收到另一个种子,即使在紧密循环中它也能正常工作。

总结一下:

所以,总结一下:

  • IEnumerable应该迭代一次,并且只迭代一次。 否则可能会给用户带来意想不到的结果。
  • 如果您可以访问比简单枚举更好的function,则不必遍历所有元素。 最好立即抓住正确的结果。
  • 考虑一下您正在仔细检查的接口。 虽然IReadOnlyList绝对是最佳候选者,但它不是从IListinheritance的,这意味着它在实践中效果会更差。

最终结果是Just Works。

T[]List都共享相同的接口: IEnumerable

IEnumerable但是,没有Length或Count成员,但有一个扩展方法Count() 。 序列上也没有索引器,因此必须使用ElementAt(int)扩展方法。

有点像:

 public static T AnyOne(this IEnumerable source) { int endExclusive = source.Count(); int randomIndex = Random.Range(0, endExclusive); return source.ElementAt(randomIndex); } 

你可以稍微改变你的定义:

 public static T AnyOne(this IEnumerable ra) { if(ra==null) throw new ArgumentNullException("ra"); int k = ra.Count(); int r = Random.Range(0,k); return ra.ElementAt(r-1); } 

现在,为实现IEnumerable接口的所有类型定义扩展方法。