这是一个非常方便的扩展,适用于array
任何内容:
public static T AnyOne<T>(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<T>(this List<T> listy) where T:class
{
int k = listy.Count;
int r = Random.Range(0,k);
return listy[r];
}
事实上,有没有办法一次性推广涵盖array
和List<>
s的泛型?或者知道不可能吗?
对我来说,答案是否可以进一步涵盖Collection
?或者确实有下面的专家之一已经实现了??
显然,它当然可以在任何c#milieu中使用。
答案 0 :(得分:8)
T[]
和List<T>
实际上都实现了IList<T>
,它提供了枚举,Count属性和索引器。
public static T AnyOne<T>(this IList<T> ra)
{
int k = ra.Count;
int r = Random.Range(0,k);
return ra[r];
}
请注意:对于 Unity3D 环境,具体来说,这是正确答案。关于这个答案IReadOnlyList<T>
的进一步改进,它在Unity3D中不可用。 (关于IEnumerable<T>
的(巧妙)扩展甚至覆盖没有计数/可索引性的对象的情况,当然在游戏引擎情况下这将是一个独特的概念(例如AnyOneEvenInefficiently
或{{1} })。)
答案 1 :(得分:6)
事实上,T[]
和List<T>
之间最恰当的公共接口是IReadOnlyList<T>
public static T AnyOne<T>(this IReadOnlyList<T> list) where T:class
{
int k = list.Count;
int r = Random.Range(0,k);
return list[r];
}
正如另一个答案所述,IList<T>
也有效,但良好的做法要求您向来电者请求方法所需的最小功能,在本例中为{{ 1}}属性和只读索引器。
Count
也可以,但它允许调用者传递非集合迭代器,其中IEnumerable<T>
和Count
扩展方法效率非常低 - 如ElementAt
,数据库查询等
Unity3D工程师的注意事项:如果你看一下IReadOnlyList Interface documentation的最底层,它可以从.Net 4.5开始使用。在早期版本的.Net中,您必须使用Enumerable.Range(0, 1000000)
(自2.0起可用)。 Unity在.Net版本上远远落后。 2016年,Unity仅使用.Net 2.0.5。因此,对于Unity3D,您必须使用IList<T>
。
答案 2 :(得分:5)
有些人选择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
会以意想不到的方式破坏应用程序的性能。例如,从设备流式传输可能会让您遇到麻烦。也就是说,你可以很好地争辩说,当这个系列的特征固有的时候会有所期待 - 而且我个人认为论证会成立。
其次 - 这可能更为重要 - 并不能保证你的枚举每次迭代都会返回相同的序列(因此也无法保证你的代码不会崩溃) )。例如,考虑一下看似无辜的代码片段,它可能对测试有用:
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次数据。我们可以通过一次浏览数据来解决这个问题。
&#39;&#39;&#39;这里实际上很简单:如果我们看过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>
,我们的表现现在会变慢,这没有任何意义,因为我们知道有更好的实施可用索引和Count
可供我们使用的属性。
我们正在寻找的是这个更好的解决方案的公分母,它们在我们能找到的尽可能多的集合中使用。我们最终得到的是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:对于更复杂的场景,请查看策略模式。
<强>随机强>
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>
&#39; s应该迭代一次,并且只能迭代一次。否则可能会给用户带来意想不到的结果。 IReadOnlyList<T>
绝对是最佳候选人,但它并非继承自IList<T>
,这意味着它在实践中效果较差。 最终结果是Just Works。
答案 3 :(得分:3)
T[]
和List<T>
都共享相同的界面:IEnumerable<T>
。
IEnumerable<T>
但是,没有Length或Count成员,但有一个扩展方法Count()
。序列上也没有索引器,因此您必须使用ElementAt(int)
扩展方法。
有些事情:
public static T AnyOne<T>(this IEnumerable<T> source)
{
int endExclusive = source.Count();
int randomIndex = Random.Range(0, endExclusive);
return source.ElementAt(randomIndex);
}
答案 4 :(得分:1)
答案是使用原始代码!
这应该是StackOverflow
上唯一的一个问题,该问题本身比提供的任何答案都说明明显更好的代码。所有建议的答案都鼓励使用接口,这将大大降低性能。不要在生产代码中使用这些解决方案!
鉴于该问题被标记为unity3d
,显然他的代码将成为游戏的一部分。在游戏中,您想要的最后一件事是由于garbage collection
而导致的间歇性停顿。通常,在Unity中,您希望枚举器具有出色的性能。这使我自己得到了答案:
除非您真的必须这样做。 List<T>
和T[]
类型具有高度优化的值类型枚举器。将类型转换为接口后,您将还原为未优化的参考类型的版本。每次对GetEnumerator()
的非优化版本的调用都会产生垃圾,加起来会导致随后的结结(请相信我),当垃圾收集器收集这些分配的对象时。
详细信息,请参阅我的other answer。
答案 5 :(得分:0)
您可以稍微更改一下定义:
public static T AnyOne<T>(this IEnumerable<T> ra)
{
if(ra==null)
throw new ArgumentNullException("ra");
int k = ra.Count();
int r = Random.Range(0,k);
return ra.ElementAt(r-1);
}
现在为实现IEnumerable<T>
接口的所有类型定义扩展方法。